metis-desk/scripts/workstation-dashboard-ui.test.py
David fa9d95fa9e feat: add workstation equipment and organizational assignment functionalities
- Implemented `workstationEquipment` to manage workstation asset assignments, including principal and secondary assets.
- Created tests for `workstationEquipment` to ensure correct behavior with various scenarios.
- Developed `workstationOrganizationalAssignments` to retrieve current assignments for a responsible person in a company.
- Added tests for `workstationOrganizationalAssignments` to validate assignment retrieval logic.
- Introduced UI components for displaying workstation profile configurations and responsible person details.
- Created compact UI components for displaying lists and tables, enhancing the user interface for asset management.
- Added utility functions for handling cell content in tables, ensuring proper rendering of data.
2026-10-24 04:13:30 +02:00

812 lines
76 KiB
Python

#!/usr/bin/env python3
"""Pruebas MCP del dashboard real sobre fixtures HTTP de la instalación desechable."""
import contextlib
import base64
import hashlib
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)
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;")
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
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")
self.assertNotIn("Activos asociados", values["headings"])
self.assertNotIn("Responsable y contexto", values["headings"])
self.assertEqual(len(values["headings"]),6)
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)
if width == 390:
self.expand_primary()
self.code("await page.locator('#workstation-configurations').scrollIntoViewIfNeeded();return true;")
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):
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;")
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)};""")
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")
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()
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()
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;")
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)
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()
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):
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)
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)}});
});""")
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)
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'])+";"))
if __name__ == "__main__":
unittest.main(verbosity=2)