metis-desk/scripts/workstation-dashboard-ui.test.py

812 lines
76 KiB
Python
Raw Normal View History

2026-10-22 12:32:31 +00:00
#!/usr/bin/env python3
"""Pruebas MCP del dashboard real sobre fixtures HTTP de la instalación desechable."""
import contextlib
import base64
import hashlib
2026-10-22 12:32:31 +00:00
import json
import os
from pathlib import Path
import runpy
import tomllib
import unittest
from urllib.parse import urlparse
ROOT = Path(__file__).resolve().parent.parent
AUDIT = runpy.run_path(str(ROOT / "scripts/visual-audit.py"))
class WorkstationDashboardTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
value = os.environ.get("METIS_PLAYWRIGHT_STORAGE_STATE")
if not value:
raise unittest.SkipTest("Requiere visual-audit.py y workstation-dashboard-fixtures.py.")
state = (ROOT / value).resolve()
if not state.is_relative_to(ROOT / ".artifacts/playwright") or state.stat().st_mode & 0o077:
raise RuntimeError("La sesión debe ser privada y pertenecer al preparador.")
report = json.loads((state.parent / "report.json").read_text())
if urlparse(report["origin"]).hostname != "localhost" or report.get("ordinaryUser") is not True:
raise RuntimeError("Requiere instalación local sintética de pruebas.")
cls.fixture = json.loads((state.parent / "workstation-fixtures.json").read_text())
if cls.fixture['origin'] != report['origin'] or cls.fixture['companyId'] != report['companyId']:
raise RuntimeError("Los escenarios no pertenecen a la instalación desechable activa.")
cls.origin = cls.fixture["origin"]
cls.base = f"/companies/{cls.fixture['companyId']}"
phase = os.environ.get("METIS_DASHBOARD_CAPTURE_PHASE", "after")
if phase not in ("before", "after"):
raise RuntimeError("Fase de captura desconocida.")
cls.output = state.parent / "workstation-dashboard-wd043" / phase
cls.output.mkdir(parents=True, exist_ok=True)
2026-10-22 12:32:31 +00:00
cls.evidence = json.loads((cls.output / "measurements.json").read_text()) if (cls.output / "measurements.json").exists() else []
cls.stack = contextlib.ExitStack()
cls.addClassCleanup(cls.stack.close)
config = tomllib.loads((ROOT / ".codex/config.toml").read_text())["mcp_servers"]["playwright"]
cls.client = cls.stack.enter_context(AUDIT["browser"](config, state))
cls.code("page.__workstationErrors=[];page.on('pageerror',error=>page.__workstationErrors.push(error.message));return true;")
@classmethod
def tearDownClass(cls):
if hasattr(cls, 'client'):
errors=cls.code("return page.__workstationErrors;")
cls.record('uncaught-page-errors',errors)
if errors:
raise AssertionError('Errores JavaScript de página: '+str(errors))
@classmethod
def tool(cls, name, args):
return AUDIT["tool"](cls.client, name, args)
@classmethod
def code(cls, body):
return AUDIT["result_json"](cls.tool("browser_run_code_unsafe", {"code": "async (page) => {" + body + "}"}))
@classmethod
def record(cls, name, values):
cls.evidence = [entry for entry in cls.evidence if entry["scenario"] != name]
cls.evidence.append({"scenario": name, "values": values})
(cls.output / "measurements.json").write_text(json.dumps(cls.evidence, ensure_ascii=False, indent=2) + "\n")
def navigate(self, key="main", pending=True):
url = self.origin + self.base + "/workstations/" + self.fixture[key]
self.tool("browser_navigate", {"url": url})
self.code("await page.locator('main h1').waitFor();return true;")
if pending:
self.code("await page.locator('[data-pending-count]').waitFor();return true;")
self.settle()
def settle(self):
self.code("await page.evaluate(async()=>{await document.fonts.ready;await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)))});return true;")
def capture(self, name):
self.record(name, AUDIT["capture"](self.client, self.output, name))
def expand_primary(self):
self.code("await page.getByRole('button',{name:'Mostrar configuraciones de Ángela · administración',exact:true}).click();await page.locator('#workstation-configurations').waitFor();return true;")
2026-10-22 12:32:31 +00:00
def test_01_responsive_and_density(self):
for width, height in [(1600,1000),(1440,900),(1280,900),(1024,768),(768,1024),(390,844)]:
self.tool("browser_resize", {"width":width,"height":height})
self.navigate()
self.code("await page.getByRole('link',{name:'Gestor documental de ejemplo',exact:true}).first().waitFor();return true;")
values = self.code("""return await page.evaluate(()=>({width:innerWidth,documentWidth:document.documentElement.scrollWidth,pending:document.querySelector('[data-pending-count]').textContent,headings:[...document.querySelectorAll('main h2')].map(x=>x.textContent.trim()),mainWidth:document.querySelector('main').getBoundingClientRect().width,sections:[...document.querySelectorAll('main section[aria-labelledby]')].map(e=>({title:e.querySelector('h2')?.textContent.trim(),...Object.fromEntries(['x','y','width','height'].map(k=>[k,e.getBoundingClientRect()[k]]))}))}));""")
equipment=self.code("const region=page.getByRole('region',{name:'Equipamiento',exact:true});return {members:await region.locator('[data-slot=compact-list-item]').count(),principalBadges:await region.getByText('Principal',{exact:true}).count(),firstHref:await region.locator('[data-slot=compact-list-item]').first().getByRole('link').getAttribute('href')};")
self.assertEqual(equipment['members'],2)
self.assertEqual(equipment['principalBadges'],1)
self.assertEqual(equipment['firstHref'],self.base+'/assets/'+self.fixture['host'])
values['equipment']=equipment
2026-10-22 12:32:31 +00:00
self.record(f"geometry-{width}", values)
self.assertLessEqual(values["documentWidth"], width)
self.assertEqual(values["pending"], "1")
self.assertNotIn("Ordenador principal", values["headings"])
self.assertEqual(values["headings"].count("Equipamiento"),1)
self.assertEqual(values["headings"][0],"Equipamiento")
2026-10-22 12:32:31 +00:00
self.assertNotIn("Activos asociados", values["headings"])
self.assertNotIn("Responsable y contexto", values["headings"])
self.assertEqual(len(values["headings"]),6)
2026-10-22 12:32:31 +00:00
self.assertNotIn("Perfiles activos", values["headings"])
self.capture(f"S01-{width}")
if width == 1600:
for label, suffix in [("Personas y perfiles","profiles"),("Equipamiento","equipment"),("Software del ordenador principal","software"),("Soporte y acceso remoto","support"),("Último cambio","latest")]:
self.code("await page.getByRole('region',{name:"+json.dumps(label)+",exact:true}).first().scrollIntoViewIfNeeded();return true;")
self.capture("S01-detail-"+suffix)
2026-10-22 12:32:31 +00:00
if width == 390:
self.expand_primary()
self.code("await page.locator('#workstation-configurations').scrollIntoViewIfNeeded();return true;")
2026-10-22 12:32:31 +00:00
self.capture('S01-configurations-390')
self.tool("browser_resize", {"width":1440,"height":900})
self.navigate("dense")
self.code("await page.getByRole('link',{name:/DASH-PROFILE/}).first().waitFor();return true;")
self.capture("S04-density-1440")
self.assertEqual(self.code("return await page.locator('main').evaluate(e=>e.scrollWidth<=e.clientWidth);"), True)
def test_02_profiles_refetch_and_compact_summary(self):
2026-10-22 12:32:31 +00:00
self.tool("browser_resize", {"width":1440,"height":900})
self.navigate()
self.code("await page.getByRole('button',{name:'1 perfil archivado',exact:true}).click();await page.getByRole('link',{name:'Ángela · perfil archivado del mismo ordenador',exact:true}).waitFor();return true;")
self.code("await page.getByRole('button',{name:'Ocultar archivados',exact:true}).click();return true;")
self.code("await page.getByRole('button',{name:'Mostrar configuraciones de Íñigo · consultas de documentación',exact:true}).click();return true;")
2026-10-22 12:32:31 +00:00
self.assertFalse(self.code("return await page.locator('#workstation-configurations').getByText('Instalación pendiente de completar.',{exact:true}).isVisible();"))
value = self.code("""const selector=page.getByRole('button',{name:'Ocultar configuraciones de Íñigo · consultas de documentación',exact:true});await selector.focus();await page.context().setOffline(true);const response=page.waitForResponse(r=>r.url().includes('/api/work-profiles/')&&r.status()===200);await page.context().setOffline(false);await response;return {selection:await page.locator('#workstation-configurations h3').textContent(),focused:await selector.evaluate(e=>e===document.activeElement)};""")
2026-10-22 12:32:31 +00:00
self.assertIn("Íñigo", value["selection"])
self.assertTrue(value["focused"])
self.record("S07-refetch", value)
self.assertEqual(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).locator('input[data-quick-search],footer').count();"),0)
self.code("await page.getByRole('region',{name:'Personas y perfiles',exact:true}).getByRole('link',{name:'Ángela · administración',exact:true}).waitFor();return true;")
self.capture("S08-compact-summary")
2026-10-22 12:32:31 +00:00
def test_03_history_edit_software_and_navigation(self):
self.navigate()
self.code("await page.getByRole('button',{name:'Ver historial',exact:true}).click();await page.getByRole('dialog').waitFor();return true;")
self.assertTrue(self.code("return await page.getByRole('dialog').getByText('Administración — puesto inicial',{exact:true}).isVisible();"))
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.assertEqual(self.code("return await page.evaluate(()=>document.activeElement?.textContent.trim());"),"Ver historial")
self.code("await page.getByRole('button',{name:'Editar puesto',exact:true}).first().click();await page.getByRole('dialog').waitFor();return true;")
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.code("await page.getByRole('button',{name:'Gestionar software',exact:true}).click();await page.getByRole('dialog').waitFor();return true;")
self.code("await page.getByRole('dialog').getByRole('button',{name:'Añadir instalación',exact:true}).waitFor();return true;")
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.assertEqual(self.code("return await page.evaluate(()=>document.activeElement?.textContent.trim());"),"Gestionar software")
target = self.base + "/assets/" + self.fixture["host"]
self.code("await page.locator('a[href="+json.dumps(target)+"]').first().click();await page.locator('main h1').waitFor();return true;")
self.assertIn(target,self.code("return page.url();"))
self.navigate()
self.code("await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).click();return true;")
self.assertTrue(self.code("return await page.locator('#workstation-configurations').evaluate(e=>e===document.activeElement);"))
self.assertTrue(self.code("return await page.getByRole('checkbox',{name:'Solo pendientes',exact:true}).isChecked();"))
self.capture("S01-configurations")
def test_04_empty_and_dismantled(self):
for key, name in [("withoutResponsible","S02"),("empty","S03"),("retired","S06")]:
self.navigate(key)
self.capture(name)
if key == "withoutResponsible":
self.assertTrue(self.code("return (await page.locator('main').innerText()).includes('Sin responsable');"))
self.assertTrue(self.code("return await page.getByRole('link',{name:'Ángela · laboratorio',exact:true}).isVisible();"))
else:
self.assertTrue(self.code("return await page.getByRole('button',{name:'Nuevo perfil',exact:true}).isDisabled();"))
self.assertFalse(self.code("return await page.getByRole('button',{name:'Gestionar software',exact:true}).isVisible();"))
def test_05_partial_errors_and_retry(self):
company = self.fixture["companyId"]
self.code("await page.route('**/api/work-profiles/"+company+"',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));await page.route('**/api/folder-mappings/"+company+"',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;")
try:
self.navigate(pending=False)
self.code("await page.getByText('No se puede confirmar el total de pendientes.',{exact:true}).waitFor();return true;")
self.assertFalse(self.code("return await page.locator('[data-pending-count]').isVisible();"))
self.assertTrue(self.code("return await page.getByRole('heading',{name:'Equipamiento',exact:true}).isVisible();"))
self.code("await page.getByRole('link',{name:'Gestor documental de ejemplo',exact:true}).first().waitFor();return true;")
self.capture("S05-profiles-and-folders-error")
finally:
self.code("await page.unroute('**/api/work-profiles/"+company+"');await page.unroute('**/api/folder-mappings/"+company+"');return true;")
self.code("await page.getByRole('button',{name:'Reintentar',exact:true}).first().click();return true;")
self.navigate()
self.code("await page.route('**/api/folder-mappings/"+company+"',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;")
try:
self.navigate(pending=False)
self.code("await page.getByText('No se puede confirmar el total de pendientes.',{exact:true}).waitFor();return true;")
self.assertTrue(self.code("return await page.getByRole('link',{name:'Ángela · administración',exact:true}).isVisible();"))
self.expand_primary()
2026-10-22 12:32:31 +00:00
self.assertTrue(self.code("return await page.locator('#workstation-configurations').getByText('Instalación pendiente de completar.',{exact:true}).isVisible();"))
self.capture("S05-folders-only-error")
finally:
self.code("await page.unroute('**/api/folder-mappings/"+company+"');return true;")
def test_06_missing_and_authentication(self):
self.tool("browser_navigate", {"url":self.origin+self.base+"/workstations/019a0a00-0000-7000-8000-000000000999"})
self.code("await page.getByText('Este puesto de trabajo no existe en esta compañía.',{exact:true}).waitFor();return true;")
status = self.code("const context=await page.context().browser().newContext();try{const response=await context.request.get("+json.dumps(self.origin+"/api/assets/"+self.fixture['companyId'])+");return response.status()}finally{await context.close()}")
self.assertEqual(status,401)
self.record("S09-authentication", {"unauthenticatedStatus":status})
def api(self, path, body=None):
return self.code("""const path=__PATH__,body=__BODY__;const origin=__ORIGIN__;
const session=await page.request.get(origin+'/api/auth/session');const {csrfToken}=await session.json();
const response=body===null?await page.request.get(origin+'/api'+path):await page.request.post(origin+'/api'+path,{data:body,headers:{origin,'x-csrf-token':csrfToken}});
if(!response.ok())throw Error('Fixture HTTP '+response.status());return response.json();
""".replace("__PATH__", json.dumps(path)).replace("__BODY__",json.dumps(body)).replace("__ORIGIN__",json.dumps(self.origin)))
def test_07_replace_main_computer(self):
company = self.fixture["companyId"]
inventory = self.api('/assets/'+company)
original = next(x for x in inventory['workstations'] if x['id']==self.fixture['main'])
replacement = next((x for x in inventory['assets'] if x['values']['name']=='DASH-S10-SWAP'),None)
if replacement is None:
catalog = self.api('/asset-catalog/'+company)
template = next(x for x in catalog['items'] if x['values'].get('name')=='Portátil' and x['values']['kind']=='template')
replacement = self.api('/assets/'+company,{'values':{'name':'DASH-S10-SWAP','templateId':template['id'],'siteId':None,'status':'operational','notes':'Fixture S10 desechable.','characteristics':{}}})['asset']
self.navigate()
self.expand_primary()
2026-10-22 12:32:31 +00:00
values = dict(original['values'],computerAssetId=replacement['id'],assetIds=[replacement['id']])
self.api('/assets/'+company+'/workstations/'+original['id']+'/update',{'values':values})
try:
self.code("await page.context().setOffline(true);await page.context().setOffline(false);return true;")
2026-10-22 12:32:31 +00:00
self.code("await page.getByRole('link',{name:'DASH-S10-SWAP',exact:true}).first().waitFor();return true;")
self.assertFalse(self.code("return await page.getByRole('link',{name:'Ángela · administración',exact:true}).isVisible();"))
self.assertEqual(self.code("return await page.locator('[data-pending-count]').textContent();"),'0')
self.assertEqual(self.code("return await page.locator('#workstation-configurations').count();"),0)
2026-10-22 12:32:31 +00:00
self.capture('S10-replaced-computer')
finally:
self.api('/assets/'+company+'/workstations/'+original['id']+'/update',{'values':original['values']})
def test_08_unknown_documentary_coverage(self):
company = self.fixture['companyId']
mapping = next(x for x in self.api('/folder-mappings/'+company)['mappings'] if x['values']['profileId']==self.fixture['primaryProfile'])
updated = self.api('/folder-mappings/'+company+'/'+mapping['id']+'/update',{'values':dict(mapping['values'],accessAccountId=None),'expectedRevisionId':mapping['history'][-1]['id']})['mapping']
try:
self.assertEqual(updated['accessAccountCoverage'],'unknown')
self.navigate()
self.expand_primary()
2026-10-22 12:32:31 +00:00
self.code("await page.locator('#workstation-configurations').scrollIntoViewIfNeeded();return true;")
label=json.loads((ROOT/'modules/companies/src/common/locales/es.json').read_text())['companies']['folders']['accountCoverage']['unknown']
self.code("await page.locator('#workstation-configurations').getByText("+json.dumps(label)+",{exact:false}).waitFor();return true;")
self.assertEqual(self.code("return await page.locator('[data-pending-count]').textContent();"),'1')
self.capture('S11-unknown-coverage')
finally:
self.api('/folder-mappings/'+company+'/'+mapping['id']+'/update',{'values':mapping['values'],'expectedRevisionId':updated['history'][-1]['id']})
def test_09_stale_profiles_and_retry(self):
self.navigate()
company=self.fixture['companyId']
self.code("await page.route('**/api/work-profiles/"+company+"',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;")
try:
self.code("await page.context().setOffline(true);await page.context().setOffline(false);await page.getByText('No se puede confirmar el total de pendientes.',{exact:true}).waitFor();return true;")
self.assertTrue(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).getByRole('link',{name:'Ángela · administración',exact:true}).isVisible();"))
self.capture('S07-stale-profiles')
finally:
self.code("await page.unroute('**/api/work-profiles/"+company+"');return true;")
self.code("await page.getByRole('button',{name:'Reintentar',exact:true}).first().click();await page.locator('[data-pending-count=\"1\"]').waitFor();return true;")
def test_10_keyboard_contrast_and_expansion(self):
2026-10-22 12:32:31 +00:00
self.tool('browser_resize',{'width':1440,'height':900})
self.navigate()
self.code("await page.getByRole('button',{name:'Ver historial',exact:true}).focus();await page.keyboard.press('Enter');await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.assertTrue(self.code("return await page.getByRole('button',{name:'Ver historial',exact:true}).evaluate(e=>e.matches(':focus-visible'));"))
self.code("await page.getByRole('button',{name:'Mostrar configuraciones de Íñigo · consultas de documentación',exact:true}).focus();await page.keyboard.press('Enter');return true;")
self.assertIn('Íñigo',self.code("return await page.locator('#workstation-configurations h3').textContent();"))
self.code("await page.keyboard.press('Space');return true;")
self.assertEqual(self.code("return await page.locator('#workstation-configurations').count();"),0)
2026-10-22 12:32:31 +00:00
ratios=self.code("""return await page.evaluate(()=>{
const rgb=color=>{const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const context=canvas.getContext('2d');context.fillStyle=color;context.fillRect(0,0,1,1);return [...context.getImageData(0,0,1,1).data].slice(0,3)};
const lum=c=>c.map(v=>v/255).map(v=>v<=0.04045?v/12.92:((v+0.055)/1.055)**2.4).reduce((s,v,i)=>s+v*[0.2126,0.7152,0.0722][i],0);
return [...document.querySelectorAll('main header button,main a,main button[aria-expanded][aria-controls],[data-pending-count]')].filter(e=>e.getBoundingClientRect().width>0).map(e=>{const colors=[];for(let n=e;n;n=n.parentElement){colors.unshift(getComputedStyle(n).backgroundColor)}const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const context=canvas.getContext('2d');context.fillStyle='#fff';context.fillRect(0,0,1,1);for(const color of colors){context.fillStyle=color;context.fillRect(0,0,1,1)}const bg=[...context.getImageData(0,0,1,1).data].slice(0,3);const fg=rgb(getComputedStyle(e).color);const values=[lum(bg),lum(fg)].sort((a,b)=>a-b);return {label:e.textContent.trim()||e.getAttribute('aria-label'),ratio:(values[1]+0.05)/(values[0]+0.05)}});
2026-10-22 12:32:31 +00:00
});""")
self.record('text-contrast',ratios)
self.assertTrue(all(x['ratio']>=4.5 for x in ratios),ratios)
self.navigate('dense')
self.assertEqual(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).locator('tbody tr[data-entity-id]').count();"),5)
self.code("const region=page.getByRole('region',{name:'Personas y perfiles',exact:true});const button=region.getByRole('button',{name:'Mostrar todos (50)',exact:true});await button.focus();await page.keyboard.press('Enter');return true;")
self.assertEqual(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).locator('tbody tr[data-entity-id]').count();"),50)
self.assertTrue(self.code("return await page.getByRole('button',{name:'Mostrar menos',exact:true}).first().evaluate(e=>e===document.activeElement);"))
self.code("await page.getByRole('button',{name:'10 perfiles archivados',exact:true}).click();return true;")
self.assertEqual(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).locator('tbody tr[data-entity-id]').count();"),60)
self.code("await page.getByRole('region',{name:'Personas y perfiles',exact:true}).getByRole('button',{name:'Mostrar menos',exact:true}).click();return true;")
self.assertEqual(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).locator('tbody tr[data-entity-id]').count();"),5)
self.code("await page.getByRole('region',{name:'Equipamiento',exact:true}).getByRole('button',{name:'Mostrar todos (80)',exact:true}).click();return true;")
self.assertEqual(self.code("return await page.getByRole('region',{name:'Equipamiento',exact:true}).locator('[data-slot=compact-list-item]').count();"),80)
2026-10-22 12:32:31 +00:00
def test_11_save_editor_and_rename(self):
company=self.fixture['companyId']
original=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==self.fixture['main'])
renamed='Administración — prueba de edición del dashboard'
self.navigate()
try:
self.code("await page.getByRole('button',{name:'Editar puesto',exact:true}).first().click();const dialog=page.getByRole('dialog');await dialog.getByRole('textbox',{name:'Nombre',exact:true}).fill("+json.dumps(renamed)+");await dialog.getByRole('button',{name:'Guardar',exact:true}).click();await dialog.waitFor({state:'hidden'});await page.getByRole('heading',{level:1,name:"+json.dumps(renamed)+",exact:true}).waitFor();return true;")
saved=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==original['id'])
self.assertEqual(len(saved['history']),len(original['history'])+1)
label=json.loads((ROOT/'modules/assets/src/common/locales/es.json').read_text())['workstationDetail']['rename']
self.code("await page.getByRole('button',{name:"+json.dumps(label)+",exact:true}).click();const field=page.getByRole('textbox',{name:'Nombre',exact:true});await field.fill("+json.dumps(original['values']['name'])+");await field.press('Enter');await page.getByRole('heading',{level:1,name:"+json.dumps(original['values']['name'])+",exact:true}).waitFor();return true;")
restored=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==original['id'])
self.assertEqual(restored['values'],original['values'])
self.assertEqual(len(restored['history']),len(original['history'])+2)
self.record('editor-save-and-inline-rename',{'saved':True,'revisionDelta':2})
finally:
current=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==original['id'])
if current['values'] != original['values']:
self.api('/assets/'+company+'/workstations/'+original['id']+'/update',{'values':original['values']})
def test_12_context_navigation_and_full_page(self):
self.tool('browser_resize',{'width':1600,'height':1000})
self.navigate()
before=self.code("return await page.locator('main').evaluate(e=>e.getBoundingClientRect().width);")
self.code("const button=page.getByRole('button',{name:'Ocultar secciones',exact:true});await button.focus();await page.keyboard.press('Enter');return true;")
self.code("await page.locator('[data-workspace-navigation-trigger=company]').waitFor();return true;")
after=self.code("return await page.locator('main').evaluate(e=>e.getBoundingClientRect().width);")
self.assertGreater(after,before)
self.code("const button=page.locator('[data-workspace-navigation-trigger=company]');await button.focus();await page.keyboard.press('Enter');await page.getByRole('dialog').waitFor();return true;")
self.assertTrue(self.code("return await page.getByRole('dialog').getByRole('link',{name:/^Personas/}).isVisible();"))
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.assertTrue(self.code("return await page.locator('[data-workspace-navigation-trigger=company]').evaluate(e=>e===document.activeElement);"))
self.capture('S01-company-navigation-hidden')
self.record('company-navigation-width',{'before':before,'after':after,'automaticCollapse':False})
self.code("await page.locator('[data-workspace-navigation-trigger=company]').click();await page.getByRole('dialog').getByRole('button',{name:'Fijar secciones',exact:true}).click();return true;")
self.code("await page.getByRole('button',{name:'Ocultar secciones',exact:true}).waitFor();return true;")
self.assertEqual(self.code("return await page.locator('main').evaluate(e=>e.getBoundingClientRect().width);"),before)
self.expand_primary()
# Solo para capturar todo el contenido: se restauran estilos y scroll al terminar.
self.code("await page.locator('main').evaluate(e=>{const saved=[];for(let n=e;n;n=n.parentElement){saved.push({element:n,style:n.getAttribute('style'),top:n.scrollTop});n.style.height='auto';n.style.maxHeight='none';n.style.overflow='visible';n.scrollTop=0;}window.__metisCaptureLayout=saved;return true;});return true;")
try:
result=self.tool('browser_take_screenshot',{'type':'png','fullPage':True,'scale':'css'})
part=next(x for x in result['content'] if x.get('type')=='image')
data=base64.b64decode(part['data'],validate=True)
(self.output/'S01-full-page.png').write_bytes(data)
self.record('S01-full-page',{'file':'S01-full-page.png','sha256':hashlib.sha256(data).hexdigest()})
finally:
self.code("await page.evaluate(()=>{for(const saved of window.__metisCaptureLayout){if(saved.style===null)saved.element.removeAttribute('style');else saved.element.setAttribute('style',saved.style);saved.element.scrollTop=saved.top;}delete window.__metisCaptureLayout;});return true;")
def test_13_save_responsible_and_composition(self):
company=self.fixture['companyId']
inventory=self.api('/assets/'+company)
original=next(x for x in inventory['workstations'] if x['id']==self.fixture['main'])
secondary=next(x for x in self.api('/work-profiles/'+company)['profiles'] if x['id']==self.fixture['secondaryProfile'])
person=next(x for x in self.api('/companies/'+company)['company']['people'] if x['id']==secondary['values']['personId'])
peripheral=next(x for x in inventory['assets'] if x['id'] in original['values']['assetIds'] and x['id']!=original['values']['computerAssetId'])
self.navigate()
try:
self.code("await page.getByRole('button',{name:'Editar puesto',exact:true}).first().click();await page.getByRole('dialog').getByRole('combobox',{name:'Responsable del puesto',exact:true}).click();await page.getByRole('option',{name:"+json.dumps(person['name'])+",exact:true}).click();return true;")
self.code("await page.getByRole('dialog').getByRole('button',{name:"+json.dumps('Retirar '+peripheral['values']['name']+' del puesto')+",exact:true}).click();await page.getByRole('dialog').getByRole('button',{name:'Guardar',exact:true}).click();await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
saved=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==original['id'])
self.assertEqual(saved['values']['personId'],person['id'])
self.assertNotIn(peripheral['id'],saved['values']['assetIds'])
self.assertEqual(saved['values']['computerAssetId'],original['values']['computerAssetId'])
self.assertTrue(self.code("return await page.locator('main header').first().getByRole('link',{name:"+json.dumps(person['name'])+",exact:true}).isVisible();"))
self.record('responsible-and-composition-save',{'saved':True,'mainPreserved':True})
finally:
self.api('/assets/'+company+'/workstations/'+original['id']+'/update',{'values':original['values']})
def test_14_create_profile_from_compact_header(self):
company=self.fixture['companyId']
profile_name='DASH-REFINEMENT-UI-CREATED'
def cleanup():
for profile in self.api('/work-profiles/'+company)['profiles']:
if profile['values']['name']==profile_name:
if profile['values']['active']:
self.api('/work-profiles/'+company+'/'+profile['id']+'/deactivate',{})
self.api('/work-profiles/'+company+'/'+profile['id']+'/delete',{})
cleanup()
self.navigate()
try:
self.code("await page.getByRole('button',{name:'Nuevo perfil',exact:true}).click();await page.getByRole('dialog').getByRole('button',{name:'Siguiente',exact:true}).click();await page.getByRole('dialog').getByRole('button',{name:'Seleccionar persona',exact:true}).click();return true;")
self.code("const picker=page.getByRole('dialog').last();await picker.locator('input[data-quick-search]').fill('DASH-PERSON-ACTIVE');await picker.getByRole('radio').first().click();await picker.getByRole('button',{name:'Seleccionar',exact:true}).click();return true;")
self.code("await page.getByRole('dialog').getByRole('button',{name:'Siguiente',exact:true}).click();await page.getByRole('dialog').getByRole('button',{name:'Siguiente',exact:true}).click();await page.getByRole('dialog').getByRole('textbox',{name:'Nombre del perfil',exact:true}).fill("+json.dumps(profile_name)+");await page.getByRole('dialog').getByRole('button',{name:'Guardar',exact:true}).click();await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
created=next(x for x in self.api('/work-profiles/'+company)['profiles'] if x['values']['name']==profile_name)
self.assertEqual(created['values']['assetId'],self.fixture['host'])
self.assertTrue(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).getByRole('link',{name:"+json.dumps(profile_name)+",exact:true}).isVisible();"))
self.record('profile-created-from-compact-header',{'created':True,'mainComputerMatched':True})
finally:
self.code("await page.keyboard.press('Escape');return true;")
cleanup()
def test_15_long_company_name_and_mobile_table(self):
company=self.fixture['companyId']
long_name='Compañía internacional de documentación técnica, investigación y soporte informático especializado'
self.tool('browser_resize',{'width':390,'height':844})
self.code("await page.route('**/api/companies/"+company+"',async route=>{const response=await route.fetch();const data=await response.json();data.company.name="+json.dumps(long_name)+";await route.fulfill({response,json:data});});return true;")
try:
self.navigate()
self.code("await page.locator('main header').first().scrollIntoViewIfNeeded();return true;")
self.assertTrue(self.code("return await page.locator('main header').first().getByRole('link',{name:"+json.dumps(long_name)+",exact:true}).isVisible();"))
self.assertTrue(self.code("return await page.evaluate(()=>document.documentElement.scrollWidth===innerWidth);"))
self.capture('S01-long-company-390')
self.code("await page.getByRole('region',{name:'Personas y perfiles',exact:true}).scrollIntoViewIfNeeded();const table=page.locator('[data-slot=compact-table] [data-slot=table-container]');await table.evaluate(e=>e.scrollLeft=e.scrollWidth);return true;")
self.assertTrue(self.code("return await page.getByRole('columnheader',{name:'Configuraciones',exact:true}).isVisible();"))
self.capture('S01-profiles-390')
finally:
self.code("await page.unroute('**/api/companies/"+company+"');return true;")
def test_16_equipment_without_principal(self):
company=self.fixture['companyId']
original=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==self.fixture['main'])
self.api('/assets/'+company+'/workstations/'+original['id']+'/update',{'values':dict(original['values'],computerAssetId=None)})
try:
self.tool('browser_resize',{'width':1440,'height':900})
self.navigate()
equipment="page.getByRole('region',{name:'Equipamiento',exact:true})"
self.assertEqual(self.code('return await '+equipment+".locator('[data-slot=compact-list-item]').count();"),2)
self.assertEqual(self.code('return await '+equipment+".getByText('Principal',{exact:true}).count();"),0)
label=json.loads((ROOT/'modules/assets/src/common/locales/es.json').read_text())['workstationDetail']['noComputer']
self.assertTrue(self.code('return await '+equipment+'.getByText('+json.dumps(label)+',{exact:true}).isVisible();'))
self.assertFalse(self.code("return await page.getByRole('button',{name:'Gestionar software',exact:true}).isVisible();"))
self.capture('S12-members-without-principal')
finally:
self.api('/assets/'+company+'/workstations/'+original['id']+'/update',{'values':original['values']})
def test_17_inconsistent_or_unresolved_principal(self):
company=self.fixture['companyId']
labels=json.loads((ROOT/'modules/assets/src/common/locales/es.json').read_text())
for scenario in ['outside','unresolved','foreign']:
patch=("station.values.computerAssetId="+json.dumps(self.fixture['lab'])+";" if scenario=='outside' else
"data.assets=data.assets.filter(asset=>asset.id!=="+json.dumps(self.fixture['host'])+");" if scenario=='unresolved' else
"data.assets=data.assets.map(asset=>asset.id==="+json.dumps(self.fixture['host'])+"?{...asset,companyId:"+json.dumps(self.fixture['otherCompany'])+"}:asset);")
self.code("await page.route('**/api/assets/"+company+"',async route=>{const response=await route.fetch();const data=await response.json();const station=data.workstations.find(x=>x.id==="+json.dumps(self.fixture['main'])+");"+patch+"await route.fulfill({response,json:data});});return true;")
try:
self.navigate()
region="page.getByRole('region',{name:'Equipamiento',exact:true})"
label=labels['workstationDashboard']['principalOutsideComposition' if scenario=='outside' else 'principalUnavailable']
self.code('await '+region+'.getByText('+json.dumps(label)+',{exact:true}).waitFor();return true;')
self.assertEqual(self.code('return await '+region+".getByText('Principal',{exact:true}).count();"),0)
self.assertEqual(self.code('return await '+region+".locator('[data-slot=compact-list-item]').count();"),2)
self.capture('S13-principal-'+scenario)
finally:
self.code("await page.unroute('**/api/assets/"+company+"');return true;")
def test_18_equipment_inventory_failure_and_recovery(self):
company=self.fixture['companyId']
labels=json.loads((ROOT/'modules/assets/src/common/locales/es.json').read_text())
self.tool('browser_resize',{'width':1440,'height':900})
self.code("await page.route('**/api/assets/"+company+"',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;")
try:
self.tool('browser_navigate',{'url':self.origin+self.base+'/workstations/'+self.fixture['main']})
self.code("await page.getByRole('region',{name:'Equipamiento',exact:true}).getByRole('button',{name:'Reintentar',exact:true}).waitFor();return true;")
region="page.getByRole('region',{name:'Equipamiento',exact:true})"
self.assertFalse(self.code('return await '+region+'.getByText('+json.dumps(labels['noWorkstationAssets'])+',{exact:true}).isVisible();'))
self.capture('S14-equipment-unavailable')
finally:
self.code("await page.unroute('**/api/assets/"+company+"');return true;")
self.code("await page.getByRole('region',{name:'Equipamiento',exact:true}).getByRole('button',{name:'Reintentar',exact:true}).click();await page.getByRole('region',{name:'Equipamiento',exact:true}).getByText('Principal',{exact:true}).waitFor();return true;")
self.assertEqual(self.code("return await page.getByRole('heading',{name:'Equipamiento',exact:true}).count();"),1)
self.capture('S14-equipment-recovered')
def test_19_equipment_expansion_survives_refetch(self):
self.tool('browser_resize',{'width':1440,'height':900})
self.navigate('dense')
self.code("const region=page.getByRole('region',{name:'Equipamiento',exact:true});await region.getByRole('button',{name:'Mostrar todos (80)',exact:true}).click();const button=region.getByRole('button',{name:'Mostrar menos',exact:true});await button.focus();await page.context().setOffline(true);const response=page.waitForResponse(r=>r.url().includes('/api/assets/')&&r.status()===200);await page.context().setOffline(false);await response;return true;")
values=self.code("const region=page.getByRole('region',{name:'Equipamiento',exact:true});return {members:await region.locator('[data-slot=compact-list-item]').count(),focused:await region.getByRole('button',{name:'Mostrar menos',exact:true}).evaluate(e=>e===document.activeElement)};")
self.assertEqual(values['members'],80)
self.assertTrue(values['focused'])
self.record('equipment-refetch',values)
def test_20_equipment_links_keyboard_and_return(self):
company=self.fixture['companyId']
inventory=self.api('/assets/'+company)
station=next(x for x in inventory['workstations'] if x['id']==self.fixture['main'])
for asset_id in station['values']['assetIds']:
self.navigate()
asset=next(x for x in inventory['assets'] if x['id']==asset_id)
self.code("const link=page.getByRole('region',{name:'Equipamiento',exact:true}).getByRole('link',{name:"+json.dumps(asset['values']['name'])+",exact:true});await link.focus();await page.keyboard.press('Enter');await page.locator('main h1').waitFor();return true;")
self.assertIn('/assets/'+asset_id,self.code('return page.url();'))
self.code("await page.goBack();await page.getByRole('region',{name:'Equipamiento',exact:true}).waitFor();return true;")
self.assertIn('/workstations/'+self.fixture['main'],self.code('return page.url();'))
self.assertEqual(self.code("return await page.getByRole('region',{name:'Equipamiento',exact:true}).getByText('Principal',{exact:true}).count();"),1)
def test_21_integrated_single_expansion(self):
self.tool('browser_resize',{'width':1440,'height':900})
self.navigate()
self.assertEqual(self.code("return await page.locator('#workstation-configurations').count();"),0)
self.assertEqual(self.code("return await page.getByRole('combobox',{name:'Seleccionar perfil',exact:true}).count();"),0)
self.assertEqual(self.code("return await page.getByRole('heading',{name:'Configuraciones por perfil',exact:true}).count();"),0)
self.expand_primary()
values=self.code("const region=page.getByRole('region',{name:'Personas y perfiles',exact:true});const button=region.getByRole('button',{name:'Ocultar configuraciones de Ángela · administración',exact:true});return {details:await region.locator('#workstation-configurations').count(),families:await region.locator('#workstation-configurations section').count(),expanded:await button.getAttribute('aria-expanded'),target:await page.locator('[id='+JSON.stringify(await button.getAttribute('aria-controls'))+']').count()};")
self.assertEqual(values,{'details':1,'families':5,'expanded':'true','target':1})
self.capture('S01-integrated-expanded')
self.code("await page.getByRole('button',{name:'Ocultar configuraciones de Ángela · administración',exact:true}).click();return true;")
self.assertEqual(self.code("return await page.locator('#workstation-configurations').count();"),0)
self.code("await page.getByRole('button',{name:'Mostrar configuraciones de Íñigo · consultas de documentación',exact:true}).click();return true;")
self.assertTrue(self.code("return await page.getByText('Este perfil no tiene configuraciones adicionales registradas.',{exact:true}).isVisible();"))
self.assertEqual(self.code("return await page.locator('#workstation-configurations a').count();"),6)
self.capture('S01-profile-without-configurations')
self.expand_primary()
self.assertEqual(self.code("return await page.locator('#workstation-configurations').count();"),1)
self.assertEqual(self.code("return await page.locator('button[aria-expanded=true][aria-controls]').filter({hasText:'Ocultar'}).count();"),1)
self.code("await page.getByRole('button',{name:'1 perfil archivado',exact:true}).click();return true;")
archived=self.code("const row=page.getByRole('link',{name:'Ángela · perfil archivado del mismo ordenador',exact:true}).locator('xpath=ancestor::tr');return {buttons:await row.getByRole('button').count(),link:await row.getByRole('link').first().getAttribute('href')};")
self.assertEqual(archived['buttons'],0)
self.assertIn('/work-profiles/',archived['link'])
self.record('WD042-single-expansion',values)
def test_22_removed_profile_closes_without_fallback(self):
self.navigate()
self.expand_primary()
company=self.fixture['companyId']
profile=self.fixture['primaryProfile']
self.code("await page.route('**/api/work-profiles/"+company+"',async route=>{const response=await route.fetch();const body=await response.json();body.profiles=body.profiles.map(p=>p.id==="+json.dumps(profile)+"?{...p,values:{...p.values,active:false}}:p);await route.fulfill({response,json:body})});return true;")
try:
self.code("await page.locator('#workstation-configurations').focus();await page.context().setOffline(true);await page.context().setOffline(false);await page.getByText('El perfil abierto se ha archivado o ya no pertenece a este ordenador. Selecciona otro perfil para consultar sus configuraciones.',{exact:true}).waitFor();return true;")
self.assertEqual(self.code("return await page.locator('#workstation-configurations').count();"),0)
self.assertFalse(self.code("return await page.getByRole('button',{name:'Mostrar configuraciones de Íñigo · consultas de documentación',exact:true}).getAttribute('aria-expanded')==='true';"))
self.code("await page.waitForFunction(()=>document.activeElement?.getAttribute('role')==='status');return true;")
self.capture('WD042-profile-archived-while-open')
finally:
self.code("await page.unroute('**/api/work-profiles/"+company+"');return true;")
def test_23_real_certificate_save_and_family_routes(self):
company=self.fixture['companyId']
original=next(x for x in self.api('/certificate-configurations/'+company)['configurations'] if x['values']['profileId']==self.fixture['primaryProfile'])
self.navigate()
self.expand_primary()
routes=self.code("return await page.locator('#workstation-configurations section a').evaluateAll(links=>links.map(link=>link.getAttribute('href')));")
expected=['folder-mappings','mail-configurations','application-configurations','printer-configurations','certificate-configurations']
self.assertEqual([x.rsplit('/',1)[-1] for x in routes],expected)
try:
self.code("await page.locator('#workstation-configurations section').filter({has:page.getByRole('heading',{name:'Certificados del perfil',exact:true})}).getByRole('link',{name:'Gestionar',exact:true}).click();return true;")
self.code("await page.getByRole('button',{name:'Editar',exact:true}).first().click();await page.getByRole('dialog').waitFor();await page.getByRole('dialog').getByRole('textbox',{name:'Notas',exact:true}).fill('Guardado real WD04.2');await page.getByRole('dialog').getByRole('button',{name:'Guardar',exact:true}).click();await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
current=next(x for x in self.api('/certificate-configurations/'+company)['configurations'] if x['id']==original['id'])
self.assertEqual(current['values']['notes'],'Guardado real WD04.2')
self.navigate()
self.expand_primary()
self.code("await page.locator('#workstation-configurations').getByText('Guardado real WD04.2',{exact:true}).waitFor();return true;")
self.capture('WD042-certificate-saved')
self.record('WD042-family-routes',routes)
finally:
current=next(x for x in self.api('/certificate-configurations/'+company)['configurations'] if x['id']==original['id'])
if current['values']!=original['values']:
self.api('/certificate-configurations/'+company+'/'+original['id']+'/update',{'values':original['values']})
def test_24_multiple_pending_and_dense_context(self):
company=self.fixture['companyId']
original=next(x for x in self.api('/certificate-configurations/'+company)['configurations'] if x['values']['profileId']==self.fixture['primaryProfile'])
profiles=self.api('/work-profiles/'+company)['profiles']
station=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==self.fixture['dense'])
dense=sorted((x for x in profiles if x['values']['active'] and x['values']['assetId']==station['values']['computerAssetId']),key=lambda x:x['values']['name'])[-1]
created=[]
try:
for profile_id in [self.fixture['secondaryProfile'],dense['id']]:
result=self.api('/certificate-configurations/'+company,{'values':dict(original['values'],profileId=profile_id,notes='Pendiente sintético WD04.2')})
created.append(result['configuration'])
self.navigate()
self.assertEqual(self.code("return await page.locator('[data-pending-count]').textContent();"),'2')
self.code("await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).click();return true;")
self.assertIn('Ángela',self.code("return await page.locator('#workstation-configurations h3').textContent();"))
self.code("await page.getByRole('button',{name:'Mostrar configuraciones de Íñigo · consultas de documentación',exact:true}).click();return true;")
self.assertTrue(self.code("return await page.locator('#workstation-configurations').getByText('Pendiente sintético WD04.2',{exact:true}).isVisible();"))
self.assertEqual(self.code("return await page.locator('[data-pending-count]').textContent();"),'2')
self.navigate('dense')
self.code("await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).click();return true;")
self.assertIn(dense['values']['name'],self.code("return await page.locator('#workstation-configurations h3').textContent();"))
self.assertEqual(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).locator('tbody tr[data-entity-id]').count();"),6)
self.assertEqual(self.code("return await page.locator('#workstation-configurations').count();"),1)
self.assertTrue(self.code("return await page.locator('#workstation-configurations').evaluate(e=>e===document.activeElement);"))
self.capture('S04-pending-outside-preview')
finally:
for configuration in created:
self.api('/certificate-configurations/'+company+'/'+configuration['id']+'/delete',{})
def test_25_loading_metadata_and_aggregate_queries(self):
company=self.fixture['companyId']
self.code("page.__wd042Requests=[];page.on('request',r=>{const path=new URL(r.url()).pathname;if(['folder-mappings','mail-configurations','application-configurations','printer-configurations','certificate-configurations'].some(kind=>path.startsWith('/api/'+kind+'/')))page.__wd042Requests.push(path)});await page.route('**/api/certificate-configurations/"+company+"',route=>{page.__heldCertificateRoute=route});return true;")
try:
self.navigate(pending=False)
self.expand_primary()
self.code("await page.locator('#workstation-configurations').getByRole('status',{name:'Cargando Certificados del perfil',exact:true}).waitFor();return true;")
self.assertEqual(self.code("return await page.locator('[data-pending-count]').count();"),0)
self.assertTrue(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).getByRole('link',{name:'Ángela · administración',exact:true}).isVisible();"))
self.capture('WD042-detail-loading')
finally:
self.code("if(page.__heldCertificateRoute){await page.__heldCertificateRoute.continue();delete page.__heldCertificateRoute}await page.unroute('**/api/certificate-configurations/"+company+"');return true;")
self.code("await page.locator('[data-pending-count]').waitFor();return true;")
before=self.code("return page.__wd042Requests.length;")
self.code("await page.getByRole('button',{name:'Mostrar configuraciones de Íñigo · consultas de documentación',exact:true}).click();await page.getByRole('button',{name:'Mostrar configuraciones de Ángela · administración',exact:true}).click();return true;")
after=self.code("return page.__wd042Requests.length;")
self.assertEqual(before,after)
self.record('WD042-aggregate-queries',{'beforeExpansionChanges':before,'afterExpansionChanges':after})
self.code("await page.route('**/api/resources/"+company+"',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;")
try:
self.navigate()
self.expand_primary()
self.code("await page.locator('#workstation-configurations').getByRole('alert').waitFor();return true;")
self.assertTrue(self.code("return await page.locator('#workstation-configurations').getByText('IMP-RED-01 · Impresora de planta compartida',{exact:true}).isVisible();"))
self.assertEqual(self.code("return await page.locator('#workstation-configurations').getByRole('region',{name:'Certificados del perfil',exact:true}).locator('[data-slot=compact-list-item]').count();"),0)
self.capture('WD042-resources-unavailable')
finally:
self.code("await page.unroute('**/api/resources/"+company+"');return true;")
self.code("await page.locator('#workstation-configurations').getByRole('button',{name:'Reintentar',exact:true}).click();await page.locator('#workstation-configurations').getByText('Instalación pendiente de completar.',{exact:true}).waitFor();return true;")
def test_26_expanded_geometry_and_dialog_refetch(self):
for width,height in [(1600,1000),(1440,900),(1280,900),(1024,768),(768,1024),(390,844)]:
self.tool('browser_resize',{'width':width,'height':height})
self.navigate()
self.expand_primary()
self.code("await page.locator('#workstation-configurations').getByText('Instalación pendiente de completar.',{exact:true}).waitFor();return true;")
values=self.code("return await page.evaluate(()=>({width:innerWidth,documentWidth:document.documentElement.scrollWidth,mainHeight:document.querySelector('main').scrollHeight,cards:[...document.querySelectorAll('main section[aria-labelledby]')].filter(e=>e.querySelector('h2')).map(e=>({title:e.querySelector('h2').textContent.trim(),height:e.getBoundingClientRect().height}))}));")
self.assertLessEqual(values['documentWidth'],width)
self.record('WD042-expanded-geometry-'+str(width),values)
self.code("await page.locator('#workstation-configurations').scrollIntoViewIfNeeded();return true;")
self.capture('S01-expanded-'+str(width))
self.tool('browser_resize',{'width':1440,'height':900})
self.navigate()
self.code("await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).click();await page.getByRole('button',{name:'Editar puesto',exact:true}).first().click();await page.getByRole('dialog').getByRole('button',{name:'Guardar',exact:true}).click();await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.assertEqual(self.code("return await page.locator('#workstation-configurations').count();"),1)
self.assertTrue(self.code("return await page.getByRole('checkbox',{name:'Solo pendientes',exact:true}).isChecked();"))
self.code("await page.context().setOffline(true);const response=page.waitForResponse(r=>r.url().includes('/api/certificate-configurations/')&&r.status()===200);await page.context().setOffline(false);await response;return true;")
self.assertTrue(self.code("return await page.getByRole('checkbox',{name:'Solo pendientes',exact:true}).isChecked();"))
self.assertIn('Ángela',self.code("return await page.locator('#workstation-configurations h3').textContent();"))
def test_27_family_navigation_and_profile_edit(self):
self.tool('browser_resize',{'width':1440,'height':900})
company=self.fixture['companyId']
original=next(p for p in self.api('/work-profiles/'+company)['profiles'] if p['id']==self.fixture['primaryProfile'])
titles=['Carpetas del perfil','Correos en dispositivos','Aplicaciones del perfil','Impresoras del perfil','Certificados del perfil']
for title in titles:
self.navigate()
self.expand_primary()
self.code("await page.locator('#workstation-configurations section').filter({has:page.getByRole('heading',{name:"+json.dumps(title)+",exact:true})}).getByRole('link',{name:'Gestionar',exact:true}).click();await page.getByRole('heading',{name:"+json.dumps(title)+",exact:true}).waitFor();return true;")
self.navigate()
self.code("await page.getByRole('region',{name:'Personas y perfiles',exact:true}).getByRole('link',{name:'Ángela · administración',exact:true}).click();await page.locator('a[href='+JSON.stringify("+json.dumps(self.base+'/work-profiles')+")+']').first().click();return true;")
try:
self.code("await page.locator('input[data-quick-search]').fill('Ángela · administración');return true;")
self.code("await page.locator('tr[data-entity-id='+JSON.stringify("+json.dumps(original['id'])+")+']').getByRole('button',{name:'Editar',exact:true}).click();await page.getByRole('dialog').waitFor();return true;")
self.code("await page.getByRole('dialog').getByRole('textbox',{name:'Nombre del perfil',exact:true}).fill('Ángela · edición WD04.2');return true;")
self.code("await page.getByRole('dialog').getByRole('button',{name:'Guardar',exact:true}).click();return true;")
self.code("await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
current=next(p for p in self.api('/work-profiles/'+company)['profiles'] if p['id']==original['id'])
self.assertEqual(current['values']['name'],'Ángela · edición WD04.2')
self.navigate()
self.code("await page.getByRole('button',{name:'Mostrar configuraciones de Ángela · edición WD04.2',exact:true}).click();return true;")
self.assertIn('edición WD04.2',self.code("return await page.locator('#workstation-configurations h3').textContent();"))
self.record('WD042-profile-edited',{'saved':True,'sameProfileId':current['id']==original['id']})
finally:
current=next(p for p in self.api('/work-profiles/'+company)['profiles'] if p['id']==original['id'])
if current['values']!=original['values']:
self.api('/work-profiles/'+company+'/'+original['id']+'/update',{'values':original['values']})
def test_28_mobile_profile_controls_remain_visible(self):
self.tool('browser_resize',{'width':390,'height':844})
self.navigate()
self.code("await page.getByRole('region',{name:'Personas y perfiles',exact:true}).scrollIntoViewIfNeeded();return true;")
values=self.code("const region=page.getByRole('region',{name:'Personas y perfiles',exact:true});const button=region.getByRole('button',{name:'Mostrar configuraciones de Ángela · administración',exact:true});const box=await button.boundingBox();const table=region.locator('[data-slot=table-container]');return {buttonX:box.x,buttonRight:box.x+box.width,width:await page.evaluate(()=>innerWidth),table:await table.evaluate(e=>({width:e.clientWidth,scroll:e.scrollWidth,left:e.scrollLeft})),person:await region.getByRole('link',{name:'Ángela Muñoz de la Peña',exact:true}).isVisible(),status:await region.getByText('Activo',{exact:true}).evaluateAll(elements=>elements.filter(e=>e.checkVisibility()).length)};")
self.assertGreaterEqual(values['buttonX'],0)
self.assertLessEqual(values['buttonRight'],values['width'])
self.assertLessEqual(values['table']['scroll'],values['table']['width'])
self.assertEqual(values['table']['left'],0)
self.assertTrue(values['person'])
self.assertGreaterEqual(values['status'],2)
self.capture('S01-mobile-profile-controls')
focus=self.code("const region=page.getByRole('region',{name:'Personas y perfiles',exact:true});await region.getByRole('link',{name:'Ángela · administración',exact:true}).focus();await page.keyboard.press('Tab');const person=await page.evaluate(()=>document.activeElement?.getAttribute('href'));await page.keyboard.press('Tab');const account=await page.evaluate(()=>document.activeElement?.getAttribute('href'));await page.keyboard.press('Tab');const expansion=await page.evaluate(()=>document.activeElement?.getAttribute('aria-label'));await page.keyboard.press('Space');return {person,account,expansion};")
self.assertIn('/people/',focus['person'])
self.assertTrue(focus['account'].endswith('/access?kind=account'))
self.assertEqual(focus['expansion'],'Mostrar configuraciones de Ángela · administración')
self.assertEqual(self.code("return await page.locator('#workstation-configurations').count();"),1)
self.capture('S01-mobile-expanded-final')
self.record('WD042-mobile-tab-order',focus)
self.assertTrue(self.code("return await page.evaluate(()=>document.documentElement.scrollWidth===innerWidth);"))
self.record('WD042-mobile-controls',values)
def open_organization(self):
self.code("await page.locator('main header').first().getByRole('button',{name:/^Consultar contexto organizativo de /}).click();await page.locator('[data-slot=popover-content]').waitFor();return true;")
def test_29_organization_responsive_and_keyboard(self):
self.code("page.__organizationRequests=[];page.on('request',r=>{if(new URL(r.url()).pathname==="+json.dumps('/api/companies/'+self.fixture['companyId'])+")page.__organizationRequests.push(r.method())});return true;")
for width,height in [(1600,1000),(1440,900),(1280,900),(1024,768),(768,1024),(390,844)]:
self.tool('browser_resize',{'width':width,'height':height})
self.navigate()
before=self.code("return page.__organizationRequests.length;")
self.code("const trigger=page.locator('main header').first().getByRole('button',{name:/^Consultar contexto organizativo de /});await trigger.focus();await page.keyboard.press('Enter');await page.locator('[data-slot=popover-content]').waitFor();return true;")
values=self.code("const popup=page.locator('[data-slot=popover-content]');const trigger=page.locator('main header').first().getByRole('button',{name:/^Consultar contexto organizativo de /});return {box:await popup.boundingBox(),width:await page.evaluate(()=>innerWidth),height:await page.evaluate(()=>innerHeight),expanded:await trigger.getAttribute('aria-expanded'),controls:await trigger.getAttribute('aria-controls'),role:await popup.getAttribute('role'),nested:await page.locator('main header a button,main header button a').count(),fields:await popup.locator('dd').allTextContents()};")
self.assertEqual(values['expanded'],'true')
self.assertEqual(values['role'],'dialog')
self.assertTrue(values['controls'])
self.assertEqual(values['nested'],0)
self.assertIn('Administración y gestión documental',values['fields'])
self.assertIn('Responsable de documentación',values['fields'])
self.assertGreaterEqual(values['box']['x'],0)
self.assertGreaterEqual(values['box']['y'],0)
self.assertLessEqual(values['box']['x']+values['box']['width'],width)
self.assertLessEqual(values['box']['y']+values['box']['height'],height)
self.record('WD043-popup-'+str(width),values)
self.capture('S01-organization-'+str(width))
self.code("await page.keyboard.press('Escape');await page.locator('[data-slot=popover-content]').waitFor({state:'hidden'});return true;")
self.assertTrue(self.code("return await page.locator('main header').first().getByRole('button',{name:/^Consultar contexto organizativo de /}).evaluate(e=>e===document.activeElement);"))
self.code("await page.keyboard.press('Space');await page.locator('[data-slot=popover-content]').waitFor();await page.keyboard.press('Escape');return true;")
self.assertEqual(before,self.code("return page.__organizationRequests.length;"))
def test_30_without_responsible_and_decommissioned(self):
for key in ['withoutResponsible','retired']:
self.navigate(key)
self.assertTrue(self.code("return await page.locator('main header').first().getByText(/^Responsable del puesto: *Sin responsable$/).isVisible();"))
self.assertEqual(self.code("return await page.getByRole('button',{name:/^Consultar contexto organizativo de /}).count();"),0)
self.assertEqual(self.code("return await page.getByRole('heading',{name:'Responsable y contexto',exact:true}).count();"),0)
self.capture('WD043-'+key)
self.code("await page.getByRole('button',{name:'Ver historial',exact:true}).click();await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');return true;")
def test_31_organization_stale_retry_and_refresh(self):
company=self.fixture['companyId']
self.tool('browser_resize',{'width':1440,'height':900})
self.navigate()
self.open_organization()
self.code("await page.locator('[data-slot=popover-content]').getByRole('link',{name:'Abrir ficha de persona',exact:true}).focus();await page.route('**/api/companies/"+company+"',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;")
try:
self.code("await page.context().setOffline(true);await page.context().setOffline(false);await page.locator('[data-slot=popover-content]').getByRole('alert').waitFor();return true;")
self.assertTrue(self.code("return await page.locator('[data-slot=popover-content]').getByText('Responsable de documentación',{exact:true}).isVisible();"))
self.assertTrue(self.code("return await page.locator('[data-slot=popover-content]').getByRole('link',{name:'Abrir ficha de persona',exact:true}).evaluate(e=>e===document.activeElement);"))
self.capture('WD043-organizational-source-error')
finally:
self.code("await page.unroute('**/api/companies/"+company+"');return true;")
self.code("await page.locator('[data-slot=popover-content]').getByRole('button',{name:'Reintentar',exact:true}).click();return true;")
self.code("await page.locator('[data-slot=popover-content]').getByRole('alert').waitFor({state:'hidden'});return true;")
self.record('WD043-retry-focus',self.code("return await page.evaluate(()=>({tag:document.activeElement?.tagName,slot:document.activeElement?.getAttribute('data-slot'),text:document.activeElement?.textContent}));"))
self.code("await page.waitForFunction(()=>document.activeElement?.matches('[data-slot=popover-title]'));return true;")
refresh=self.code("const pattern='**/api/companies/"+company+"';const releases=[];let holding=true;await page.route(pattern,async route=>{if(holding)await new Promise(resolve=>releases.push(resolve));await route.continue()});try{const response=page.waitForResponse(r=>new URL(r.url()).pathname==="+json.dumps('/api/companies/'+company)+"&&r.status()===200);await page.context().setOffline(true);await page.context().setOffline(false);await page.locator('[data-slot=popover-content]').getByRole('status').waitFor();const retained=await page.locator('[data-slot=popover-content]').getByText('Responsable de documentación',{exact:true}).isVisible();holding=false;for(const release of releases)release();await response;await page.locator('[data-slot=popover-content]').getByRole('status').waitFor({state:'hidden'});await page.keyboard.press('Escape');return {retained,recovered:true}}finally{holding=false;for(const release of releases)release();await page.unroute(pattern)}")
self.assertTrue(refresh['retained'])
self.assertTrue(refresh['recovered'])
self.record('WD043-refresh-retained',refresh)
source=self.api('/companies/'+company)['company']
next(x for x in source['assignments'] if x['position']=='Responsable de documentación')['position']='Cargo organizativo actualizado WD04.3'
self.open_organization()
self.code("await page.locator('[data-slot=popover-content]').getByRole('link',{name:'Abrir ficha de persona',exact:true}).focus();await page.route('**/api/companies/"+company+"',route=>route.fulfill({status:200,contentType:'application/json',body:JSON.stringify({company:"+json.dumps(source)+"})}));return true;")
try:
self.code("await page.context().setOffline(true);await page.context().setOffline(false);await page.locator('[data-slot=popover-content]').getByText('Cargo organizativo actualizado WD04.3',{exact:true}).waitFor();return true;")
self.assertTrue(self.code("return await page.locator('[data-slot=popover-content]').getByRole('link',{name:'Abrir ficha de persona',exact:true}).evaluate(e=>e===document.activeElement);"))
self.capture('WD043-organization-updated')
ratios=self.code("""return await page.evaluate(()=>{
const rgb=color=>{const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const context=canvas.getContext('2d');context.fillStyle=color;context.fillRect(0,0,1,1);return [...context.getImageData(0,0,1,1).data].slice(0,3)};
const lum=c=>c.map(v=>v/255).map(v=>v<=0.04045?v/12.92:((v+0.055)/1.055)**2.4).reduce((s,v,i)=>s+v*[0.2126,0.7152,0.0722][i],0);
return [...document.querySelectorAll('[data-slot=popover-content] h2,[data-slot=popover-content] p,[data-slot=popover-content] dt,[data-slot=popover-content] dd,[data-slot=popover-content] a,[data-slot=popover-content] button,[data-slot=popover-content] [data-slot=badge]')].filter(e=>e.getBoundingClientRect().width>0).map(e=>{const colors=[];for(let n=e;n;n=n.parentElement){colors.unshift(getComputedStyle(n).backgroundColor)}const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const context=canvas.getContext('2d');context.fillStyle='#fff';context.fillRect(0,0,1,1);for(const color of colors){context.fillStyle=color;context.fillRect(0,0,1,1)}const bg=[...context.getImageData(0,0,1,1).data].slice(0,3);const fg=rgb(getComputedStyle(e).color);const values=[lum(bg),lum(fg)].sort((a,b)=>a-b);return {label:e.textContent.trim()||e.getAttribute('aria-label'),ratio:(values[1]+0.05)/(values[0]+0.05)}});
});""")
self.assertTrue(all(x['ratio']>=4.5 for x in ratios),ratios)
self.record('WD043-popup-contrast',ratios)
finally:
self.code("await page.unroute('**/api/companies/"+company+"');return true;")
def test_32_multiple_historical_empty_and_long_organization(self):
company=self.fixture['companyId']
original=self.api('/companies/'+company)['company']
station=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==self.fixture['main'])
person_id=station['values']['personId']
assignment=next(x for x in original['assignments'] if x['personId']==person_id)
import copy
for mode in ['empty','historical','multiple','ambiguous','long']:
data=copy.deepcopy(original)
data['assignments']=[x for x in data['assignments'] if x['personId']!=person_id]
if mode=='historical':
data['assignments'].extend([dict(assignment,startDate='2000-01-01',endDate='2001-01-01'),dict(assignment,id='019a0a00-0000-7000-8000-000000000701',startDate='2098-01-01',endDate='2099-01-01')])
if mode in ['multiple','ambiguous']:
department=dict(data['departments'][0],id='019a0a00-0000-7000-8000-000000000702',name='Departamento secundario de pruebas')
data['departments'].append(department)
data['assignments'].extend([dict(assignment,endDate='2099-01-01' if mode=='ambiguous' else None),dict(assignment,id='019a0a00-0000-7000-8000-000000000703',departmentId=department['id'],isPrimary=mode=='ambiguous',position='Coordinación documental secundaria',endDate='2099-01-01' if mode=='ambiguous' else None)])
if mode=='long':
data['name']='Compañía internacional de documentación técnica, investigación y soporte informático especializado'
next(x for x in data['people'] if x['id']==person_id)['name']='Ángela Muñoz de la Peña Fernández del Río — documentación técnica y atención administrativa'
next(x for x in data['sites'] if x['id']==station['values']['siteId'])['name']='Sede administrativa y técnica central — documentación, investigación y soporte de operaciones'
data['departments'][0]['name']='Administración, documentación técnica y coordinación de servicios de soporte corporativos'
data['assignments'].append(dict(assignment,position='Responsable de documentación, coordinación técnica y atención administrativa de operaciones'))
self.tool('browser_resize',{'width':390 if mode=='long' else 1440,'height':844 if mode=='long' else 900})
self.code("await page.route('**/api/companies/"+company+"',route=>route.fulfill({status:200,contentType:'application/json',body:JSON.stringify({company:"+json.dumps(data)+"})}));return true;")
try:
self.navigate()
self.open_organization()
result=self.code("const popup=page.locator('[data-slot=popover-content]');return {fields:await popup.locator('dd').allTextContents(),primary:await popup.getByText('Principal',{exact:true}).count(),box:await popup.boundingBox(),width:await page.evaluate(()=>innerWidth),overflow:await page.evaluate(()=>document.documentElement.scrollWidth-innerWidth)};")
if mode in ['empty','historical']:
self.assertEqual(result['fields'],['—','—'])
elif mode in ['multiple','ambiguous']:
self.assertIn('Coordinación documental secundaria',result['fields'])
self.assertEqual(result['primary'],2 if mode=='ambiguous' else 1)
self.assertEqual(len(result['fields']),4)
else:
self.assertEqual(result['overflow'],0)
self.assertGreaterEqual(result['box']['x'],0)
self.assertLessEqual(result['box']['x']+result['box']['width'],390)
self.record('WD043-'+mode,result)
self.capture('WD043-organization-'+mode)
finally:
self.code("await page.unroute('**/api/companies/"+company+"');return true;")
def test_33_header_links(self):
self.tool('browser_resize',{'width':1440,'height':900})
company=self.fixture['companyId']
source=self.api('/assets/'+company)
station=next(x for x in source['workstations'] if x['id']==self.fixture['main'])
for path in [self.base,self.base+'/sites',self.base+'/people/'+station['values']['personId']]:
self.navigate()
self.code("await page.locator('main header').first().locator('a[href='+JSON.stringify("+json.dumps(path)+")+']').click();await page.waitForURL(url=>url.pathname==="+json.dumps(path)+");return true;")
self.assertTrue(self.code("return new URL(page.url()).pathname==="+json.dumps(path)+";"))
self.navigate()
self.open_organization()
self.code("await page.locator('[data-slot=popover-content]').getByRole('link',{name:'Abrir ficha de persona',exact:true}).click();return true;")
self.assertTrue(self.code("return new URL(page.url()).pathname==="+json.dumps(self.base+'/people/'+station['values']['personId'])+";"))
2026-10-22 12:32:31 +00:00
if __name__ == "__main__":
unittest.main(verbosity=2)