- Add softwareReferenceHref helper function for generating resource links. - Create useGlobalResourceEditor hook for managing global application resources. - Implement useGlobalResourceForm hook for handling global resource forms. - Introduce GlobalSoftwareCatalogProvider for context management of global applications. - Develop GlobalOperatingSystemsPage to render global operating systems using the existing GlobalApplicationsPage. - Add operating system form identity helpers for managing OS-specific fields. - Create operating system specification labels for better localization. - Implement OperatingSystemIdentityReview component for reviewing OS identities. - Add OperatingSystemProductFields component for displaying OS product details. - Write tests for resource characteristic control to ensure proper translation and value handling. - Create a Python script for auditing global operating systems UI interactions. - Add tests for expanded workstation profiles to ensure proper functionality and responsiveness.
184 lines
20 KiB
Python
184 lines
20 KiB
Python
"""SO globales: recorrido HTTP real y Playwright MCP en instalación desechable."""
|
|
import json
|
|
import base64
|
|
import hashlib
|
|
import time
|
|
import uuid
|
|
|
|
|
|
def verify(context, fixture, admin_password):
|
|
api, tool, capture = context['api'], context['tool'], context['capture']
|
|
origin, directory = context['origin'], context['directory']
|
|
checks, screenshots, measurements = [], [], []
|
|
base = '/catalogs/global-operating-systems'
|
|
|
|
def code(client, source):
|
|
print(' SO MCP: ' + source[:90], flush=True)
|
|
try:
|
|
return tool(client, 'browser_run_code_unsafe', {'code': 'async(page)=>{' + source + '}'})
|
|
except RuntimeError:
|
|
shot(client, 'failure')
|
|
diagnostic = context['result_json'](tool(client, 'browser_run_code_unsafe', {'code': "async(page)=>({alerts:await page.locator('[role=alert]').allTextContents(),dialogs:await page.getByRole('dialog').allTextContents(),buttons:await page.getByRole('button').evaluateAll(items=>items.filter(x=>x.getBoundingClientRect().width).map(x=>({text:x.innerText,disabled:x.disabled})))})"}))
|
|
measurements.append({'failure': diagnostic})
|
|
raise
|
|
|
|
def login(client, email, password):
|
|
tool(client, 'browser_navigate', {'url': origin + '/login'})
|
|
code(client, "await page.getByRole('button',{name:'Entrar',exact:true}).waitFor();")
|
|
tool(client, 'browser_fill_form', {'fields': [
|
|
{'name': 'Correo electrónico', 'type': 'textbox', 'target': 'input[type=email]', 'value': email},
|
|
{'name': 'Contraseña', 'type': 'textbox', 'target': 'input[type=password]', 'value': password}]})
|
|
tool(client, 'browser_click', {'target': 'button[type=submit]'})
|
|
code(client, "await page.locator('[data-slot=workspace]').waitFor({timeout:30000});")
|
|
|
|
def shot(client, name):
|
|
screenshots.append(capture(client, directory, 'global-os-' + name))
|
|
|
|
def report():
|
|
data = {'checks': checks, 'screenshots': screenshots, 'measurements': measurements, 'database': 'metis_desk_test', 'companies': 15,
|
|
'principal': {'people': 50, 'assetsAtLeast': 50, 'resourcesAtLeast': 50, 'profiles': 50, 'workstations': 50},
|
|
'mcp': 'Docker stdio de .codex/config.toml', 'productionEnabled': False}
|
|
(directory / 'global-operating-systems-report.json').write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n')
|
|
|
|
with context['browser'](context['config']) as client:
|
|
try:
|
|
login(client, 'setup@audit.example.test', admin_password)
|
|
tool(client, 'browser_resize', {'width': 1280, 'height': 1000})
|
|
tool(client, 'browser_navigate', {'url': origin + base})
|
|
code(client, "await page.getByText('Todavía no hay sistemas operativos globales.',{exact:false}).waitFor();")
|
|
checks.append('empty-catalog')
|
|
shot(client, 'empty')
|
|
code(client, "await page.getByRole('button',{name:'Nuevo sistema operativo global',exact:true}).click(); await page.getByRole('dialog').getByRole('textbox',{name:'Nombre',exact:true}).fill('Windows 11 Pro'); await page.getByRole('textbox',{name:'Fabricante / proveedor',exact:true}).fill('Microsoft'); await page.getByRole('textbox',{name:'Familia',exact:true}).fill('Windows'); await page.getByRole('textbox',{name:'Versión principal del producto',exact:true}).fill('11'); await page.getByRole('combobox',{name:'Estado de edición',exact:true}).click(); await page.getByRole('option',{name:'Edición conocida',exact:true}).click(); await page.getByRole('textbox',{name:'Edición',exact:true}).fill('Pro');")
|
|
code(client, "if(!await page.getByRole('button',{name:'Guardar',exact:true}).isDisabled())throw Error('Revisión obligatoria'); await page.getByRole('button',{name:'Comprobar identidad en el catálogo',exact:true}).click(); await page.getByText('No hay otra identidad equivalente en el catálogo.',{exact:false}).waitFor(); await page.getByRole('checkbox',{name:'He revisado la identidad y el catálogo; este producto es distinto.',exact:true}).check(); await page.getByRole('textbox',{name:'Edición',exact:true}).fill('Home'); if(!await page.getByRole('button',{name:'Guardar',exact:true}).isDisabled())throw Error('Revisión obsoleta'); await page.getByRole('textbox',{name:'Edición',exact:true}).fill('Pro');")
|
|
checks.extend(['identity-fields', 'known-edition', 'review-required', 'identity-review-invalidation'])
|
|
shot(client, 'create-review')
|
|
code(client, "await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('heading',{name:'Windows 11 Pro',exact:true}).waitFor(); await page.getByRole('heading',{name:'Historial',exact:true}).waitFor();")
|
|
product_url = context['result_json'](code(client, 'return page.url();'))
|
|
product_id = product_url.split('/')[-1]
|
|
checks.extend(['create-in-browser', 'history'])
|
|
shot(client, 'created-detail')
|
|
code(client, "await page.getByRole('button',{name:'Editar sistema operativo global',exact:true}).click(); await page.getByRole('textbox',{name:'Versión principal del producto',exact:true}).fill('10'); await page.getByRole('textbox',{name:'Nombre',exact:true}).fill('Windows 10 Pro'); await page.getByRole('button',{name:'Comprobar identidad en el catálogo',exact:true}).click(); await page.getByRole('checkbox',{name:'He revisado la identidad y el catálogo; este producto es distinto.',exact:true}).check(); await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByText('Este producto ya se publicó.',{exact:false}).waitFor();")
|
|
checks.append('server-rejects-product-transformation-form-preserved')
|
|
shot(client, 'identity-error')
|
|
code(client, "await page.getByRole('button',{name:'Cancelar',exact:true}).click(); await page.getByRole('dialog').waitFor({state:'hidden'}); if(!await page.getByRole('button',{name:'Editar sistema operativo global',exact:true}).evaluate(el=>el===document.activeElement))throw Error('Retorno de foco');")
|
|
checks.append('dialog-focus-return')
|
|
api.request('/auth/login', {'email': 'setup@audit.example.test', 'password': admin_password})
|
|
rc = api.request('/resource-catalog/global')['items']
|
|
ids = {item['builtinKey']: item['id'] for item in rc if item.get('builtinKey')}
|
|
identity_keys = {'provider': 'provider', 'family': 'os_family', 'productVersion': 'os_product_version', 'editionState': 'os_edition_state', 'edition': 'software_edition'}
|
|
for index in range(50):
|
|
identity = {'provider': 'Microsoft', 'family': 'Windows', 'productVersion': str(100 + index), 'editionState': 'known', 'edition': 'Pro'}
|
|
creation = str(uuid.UUID(int=(int(time.time()*1000)<<80) | (0x7<<76) | (0x2<<62) | (uuid.uuid4().int & ((1<<62)-1))))
|
|
api.request('/resource-global-operating-systems', {'creationId': creation, 'reviewedIdentity': identity,
|
|
'values': {'name': f'Windows {100+index} Pro — Producto sintético Álvarez', 'templateId': ids['template:os'], 'active': True, 'notes': '', 'characteristics': {ids['field:'+identity_keys[key]]: value for key, value in identity.items()}}}, expected=201)
|
|
company = fixture['company']
|
|
local = api.request('/resources/' + company, {'values': {'name': 'Windows 11 Pro', 'templateId': ids['template:os'], 'active': True, 'notes': '', 'characteristics': {}}}, expected=201)['resource']
|
|
for resource_id in [product_id, local['id']]:
|
|
api.request('/software-installations/' + company, {'values': {'assetId': fixture['softwareAssets'][0], 'resourceId': resource_id, 'licenseResourceId': None, 'version': '24H2', 'state': 'configured', 'notes': ''}}, expected=201)
|
|
api.request('/auth/logout', {})
|
|
tool(client, 'browser_navigate', {'url': origin + base})
|
|
code(client, "await page.getByRole('link',{name:'Windows 11 Pro',exact:true}).waitFor(); await page.getByRole('button',{name:'Siguiente',exact:true}).click(); await page.getByRole('button',{name:'Anterior',exact:true}).click(); await page.getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).fill('SIN-COINCIDENCIAS-SO'); await page.getByText('No hay sistemas operativos que coincidan.',{exact:false}).waitFor();")
|
|
checks.extend(['51-global-products', 'cursor-pagination', 'search-empty-recovery'])
|
|
shot(client, 'no-results')
|
|
code(client, "await page.getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).fill(''); await page.getByRole('link',{name:'Windows 11 Pro',exact:true}).waitFor();")
|
|
for theme in ['light', 'dark']:
|
|
code(client, "await page.evaluate(theme=>{document.documentElement.classList.toggle('dark',theme==='dark');}," + json.dumps(theme) + ");")
|
|
for width in [320,390,768,1024,1280,1600]:
|
|
tool(client, 'browser_resize', {'width': width, 'height': 1000 if width > 768 else 844})
|
|
code(client, "if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth+1))throw Error('Desbordamiento de página');")
|
|
shot(client, f'list-{theme}-{width}')
|
|
contrast = context['result_json'](code(client, """return await page.evaluate(()=>{
|
|
const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const ctx=canvas.getContext('2d',{willReadFrequently:true});
|
|
const rgba=color=>{ctx.clearRect(0,0,1,1);ctx.fillStyle=color;ctx.fillRect(0,0,1,1);return [...ctx.getImageData(0,0,1,1).data].map((v,i)=>i===3?v/255:v)};
|
|
const over=(a,b)=>{const alpha=a[3]+b[3]*(1-a[3]);return [...a.slice(0,3).map((v,i)=>(v*a[3]+b[i]*b[3]*(1-a[3]))/alpha),alpha]};
|
|
const bg=e=>e?over(rgba(getComputedStyle(e).backgroundColor),bg(e.parentElement)):[255,255,255,1];
|
|
const lum=c=>c.slice(0,3).map(v=>v/255).map(v=>v<=.04045?v/12.92:((v+.055)/1.055)**2.4).reduce((sum,v,i)=>sum+v*[.2126,.7152,.0722][i],0);
|
|
return [...document.querySelectorAll('main h1,main p,main table a,main th')].filter(e=>e.getBoundingClientRect().width&&e.innerText.trim()).map(e=>{const a=lum(over(rgba(getComputedStyle(e).color),bg(e))),b=lum(bg(e));return {text:e.innerText.slice(0,80),ratio:(Math.max(a,b)+.05)/(Math.min(a,b)+.05)}});
|
|
});"""))
|
|
if not contrast or any(item['ratio'] < 4.5 for item in contrast):
|
|
raise RuntimeError('Contraste de texto insuficiente en ' + theme + ': ' + json.dumps(contrast))
|
|
measurements.append({'theme': theme, 'contrast': contrast})
|
|
checks.append('computed-text-contrast-' + theme)
|
|
checks.append('list-' + theme + '-320-390-768-1024-1280-1600')
|
|
tool(client, 'browser_resize', {'width': 390, 'height': 844})
|
|
code(client, "await page.getByRole('button',{name:'Nuevo sistema operativo global',exact:true}).click(); await page.getByRole('dialog').waitFor(); if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth+1))throw Error('Desbordamiento de modal');")
|
|
shot(client, 'mobile-form')
|
|
code(client, "await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'});")
|
|
checks.append('mobile-form-keyboard-close')
|
|
tool(client, 'browser_resize', {'width': 1280, 'height': 1000})
|
|
code(client, "page.osPattern='**/api/resource-global-operating-systems?*'; await page.route(page.osPattern,r=>r.fulfill({status:500,contentType:'application/json',json:{code:'INTERNAL_ERROR'}})); await page.goto(" + json.dumps(origin+base) + "); await page.getByText('No se han podido cargar los sistemas operativos.',{exact:true}).waitFor();")
|
|
shot(client, 'list-error')
|
|
code(client, "await page.unroute(page.osPattern); await page.getByRole('button',{name:'Reintentar',exact:true}).click(); await page.getByRole('link',{name:'Windows 11 Pro',exact:true}).waitFor();")
|
|
checks.append('transport-error-retry')
|
|
# Una sola llamada conserva las barreras y sus callbacks en el mismo ámbito.
|
|
loading_capture = context['result_json'](code(client, """
|
|
const releases=[], pending=[];
|
|
const pattern='**/api/resource-global-operating-systems?*';
|
|
const hold=async route=>{const task=(async()=>{await new Promise(resolve=>releases.push(resolve));await route.continue()})();pending.push(task);await task};
|
|
await page.route(pattern,hold);
|
|
let screenshot;
|
|
try {
|
|
await page.goto(""" + json.dumps(origin+base) + """,{waitUntil:'domcontentloaded'});
|
|
await page.getByText('Cargando sistemas operativos…',{exact:true}).waitFor();
|
|
screenshot=(await page.screenshot({fullPage:false})).toString('base64');
|
|
} finally {
|
|
for(const release of releases)release();
|
|
await Promise.all(pending);
|
|
await page.unroute(pattern,hold);
|
|
}
|
|
await page.getByRole('link',{name:'Windows 11 Pro',exact:true}).waitFor();
|
|
return screenshot;
|
|
"""))
|
|
png = base64.b64decode(loading_capture, validate=True)
|
|
(directory/'global-os-list-loading.png').write_bytes(png)
|
|
screenshots.append({'file': 'global-os-list-loading.png', 'sha256': hashlib.sha256(png).hexdigest()})
|
|
checks.append('initial-loading')
|
|
code(client, "await page.getByRole('button',{name:'Nuevo sistema operativo global',exact:true}).click(); await page.getByRole('textbox',{name:'Nombre',exact:true}).fill('Borrador de SO'); await page.getByRole('checkbox',{name:'Publicar como producto activo',exact:true}).uncheck(); await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('heading',{name:'Borrador de SO',exact:true}).waitFor();")
|
|
checks.append('inactive-draft-in-browser')
|
|
code(client, "await page.getByRole('button',{name:'Editar sistema operativo global',exact:true}).click(); await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'}); await page.getByLabel('Menú de usuario',{exact:true}).click(); await page.getByRole('combobox',{name:'Idioma',exact:true}).click(); await page.getByRole('option',{name:'English',exact:true}).click(); await page.getByRole('combobox',{name:'Language',exact:true}).waitFor(); await page.getByLabel('User menu',{exact:true}).click(); await page.getByRole('button',{name:'Edit global operating system',exact:true}).click(); await page.getByRole('textbox',{name:'Major product version',exact:true}).waitFor(); await page.getByRole('combobox',{name:'Edition status',exact:true}).click(); await page.getByRole('option',{name:'Edition not applicable',exact:true}).waitFor(); await page.keyboard.press('Escape');")
|
|
shot(client, 'english-form')
|
|
checks.append('english-form-labels-edition-options')
|
|
code(client, "await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'});")
|
|
|
|
finally:
|
|
report()
|
|
with context['browser'](context['config']) as client:
|
|
try:
|
|
login(client, fixture['email'], fixture['password'])
|
|
tool(client, 'browser_resize', {'width': 1280, 'height': 1000})
|
|
tool(client, 'browser_navigate', {'url': product_url})
|
|
code(client, "await page.getByRole('heading',{name:'Windows 11 Pro',exact:true}).waitFor(); await page.getByText('Versión principal del producto',{exact:true}).waitFor(); if(await page.getByRole('button',{name:'Editar sistema operativo global',exact:true}).count())throw Error('Administración ordinaria');")
|
|
checks.append('ordinary-public-projection')
|
|
shot(client, 'ordinary-detail')
|
|
asset_url = origin + '/companies/' + company + '/assets/' + fixture['softwareAssets'][0]
|
|
station_url = origin + '/companies/' + company + '/workstations/' + fixture['softwareWorkstations'][0]
|
|
for view, url in [('asset',asset_url),('workstation',station_url)]:
|
|
tool(client, 'browser_navigate', {'url': url})
|
|
code(client, "await page.locator('[data-current-operating-systems]').getByRole('link',{name:'Windows 11 Pro',exact:true}).first().waitFor(); if(!await page.locator('[data-current-operating-systems]').locator('a[href=\"" + base + '/' + product_id + "\"]').count())throw Error('Destino SO global');")
|
|
code(client, "await page.locator('[data-current-operating-systems]').locator('xpath=ancestor::section[1]').scrollIntoViewIfNeeded();")
|
|
shot(client, view + '-current-os')
|
|
checks.append(view + '-global-os-navigation')
|
|
global_href = base + '/' + product_id
|
|
if view == 'asset':
|
|
code(client, "const row=page.getByRole('row').filter({has:page.locator('a[href=\"" + global_href + "\"]')}); await row.getByRole('button',{name:'Acciones',exact:true}).click(); await page.getByRole('menuitem',{name:/Historial/}).click(); await page.getByRole('dialog',{name:/Historial/}).locator('a[href=\"" + global_href + "\"]').waitFor();")
|
|
shot(client, 'installation-history-global-link')
|
|
code(client, "await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'});")
|
|
checks.append('installation-panel-and-history-global-os-navigation')
|
|
else:
|
|
code(client, "await page.locator('[data-workplace-software] a[href=\"" + global_href + "\"]').waitFor(); await page.locator('[data-workplace-map]').scrollIntoViewIfNeeded();")
|
|
shot(client, 'workplace-map-global-os')
|
|
code(client, "await page.locator('[data-workplace-software] a[href=\"" + global_href + "\"]').click(); await page.getByRole('heading',{name:'Windows 11 Pro',exact:true}).waitFor(); if(new URL(page.url()).pathname!==" + json.dumps(global_href) + ")throw Error('Destino del mapa');")
|
|
checks.append('map-click-opens-global-os-detail')
|
|
tool(client, 'browser_navigate', {'url': station_url})
|
|
code(client, "await page.locator('[data-workplace-equipment=principal] a').click(); await page.getByRole('heading',{name:'AUDIT-R23-PORT-01',exact:true}).waitFor();")
|
|
checks.append('map-equipment-pointer-navigation')
|
|
|
|
tool(client, 'browser_navigate', {'url': asset_url})
|
|
code(client, "await page.getByRole('button',{name:'Añadir instalación',exact:true}).click(); await page.getByRole('dialog',{name:'Añadir instalación',exact:true}).getByRole('button',{name:'Software',exact:true}).click(); await page.getByRole('dialog',{name:'Seleccionar software',exact:true}).getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).fill('Windows 11 Pro'); await page.getByRole('dialog',{name:'Seleccionar software',exact:true}).getByRole('cell',{name:'Windows 11 Pro',exact:true}).first().waitFor(); if(await page.getByRole('dialog',{name:'Seleccionar software',exact:true}).getByRole('cell',{name:'Windows 11 Pro',exact:true}).count()!==2)throw Error('Coexistencia');")
|
|
shot(client, 'mixed-picker')
|
|
checks.append('mixed-picker-global-local-no-replacement')
|
|
code(client, "await page.keyboard.press('Escape'); await page.keyboard.press('Escape');")
|
|
finally:
|
|
report()
|
|
print('SO global: recorrido MCP verificado. Evidencias: ' + str(directory.relative_to(context['ROOT'])), flush=True)
|