metis-desk/scripts/workstation-uiux-v21.test.py

308 lines
33 KiB
Python
Raw Normal View History

2026-10-24 23:19:17 +00:00
#!/usr/bin/env python3
"""Capturas comparables y recorridos MCP de M03/M04/M06 sobre el preparador desechable."""
import contextlib
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"))
SIZES = [(1600, 1000), (1440, 900), (1280, 900), (1024, 768), (768, 1024), (390, 844), (320, 720)]
class WorkstationUiuxV21Tests(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 las fixtures de puestos.")
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())
cls.fixture = json.loads((state.parent / "workstation-fixtures.json").read_text())
cls.origin = report["origin"]
if urlparse(cls.origin).hostname != "localhost" or report.get("ordinaryUser") is not True or cls.fixture['origin'] != cls.origin or cls.fixture['companyId'] != report['companyId']:
raise RuntimeError("Requiere la instalación sintética desechable activa.")
cls.phase = os.environ.get("METIS_UIUX_PHASE", "despues")
if cls.phase not in ("antes", "despues"):
raise RuntimeError("Fase desconocida.")
cls.output = ROOT / "docs/auditorias/ficha-puesto-trabajo/implementacion-v2.1"
(cls.output / cls.phase).mkdir(parents=True, exist_ok=True)
cls.measurements = json.loads((cls.output / "mediciones.json").read_text()) if (cls.output / "mediciones.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.base = cls.origin + "/companies/" + cls.fixture['companyId']
cls.code("page.__uiuxErrors=[];page.on('pageerror',e=>page.__uiuxErrors.push(e.message));return true;")
@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 save(cls, key, value):
cls.measurements = json.loads((cls.output / "mediciones.json").read_text()) if (cls.output / "mediciones.json").exists() else {}
cls.measurements.setdefault(cls.phase, {})[key] = value
(cls.output / "mediciones.json").write_text(json.dumps(cls.measurements, ensure_ascii=False, indent=2) + "\n")
def navigate(self, key="main"):
self.tool("browser_navigate", {"url": self.base + "/workstations/" + self.fixture[key]})
self.code("await page.locator('main h1').waitFor();await page.locator('[data-pending-count]').waitFor();return true;")
if key == "main":
self.code("await page.getByRole('link',{name:'Ángela · administración',exact:true}).waitFor();await page.getByText('equipo-adm-01.example.invalid',{exact:false}).first().waitFor();await page.getByText('Autorizada',{exact:true}).waitFor();return true;")
if key == 'main':
login='angela.munoz.documentacion.administracion.'*3+'example.invalid'
self.code("await page.getByRole('region',{name:/^Personas y perfiles/}).getByRole('link',{name:"+json.dumps(login)+",exact:true}).waitFor();return true;")
self.code("await page.getByRole('region',{name:/^Personas y perfiles/}).locator('table').waitFor();return true;")
if key == 'dense':
self.code("await page.getByRole('region',{name:/^Personas y perfiles/}).locator('tbody tr[data-entity-id]').first().waitFor();return true;")
self.code("await page.evaluate(async()=>{await document.fonts.ready;await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)))});return true;")
def api(self, path, data=None):
return self.code("const origin="+json.dumps(self.origin)+",path="+json.dumps(path)+",data="+json.dumps(data)+";const {csrfToken}=await(await page.request.get(origin+'/api/auth/session')).json();const r=data===null?await page.request.get(origin+'/api'+path):await page.request.post(origin+'/api'+path,{data,headers:{origin,'x-csrf-token':csrfToken}});if(!r.ok())throw Error('HTTP '+r.status());return r.json();")
def capture(self, name):
image = AUDIT["capture"](self.client, self.output / self.phase, name)
geometry = self.code("""return await page.evaluate(()=>{const rect=e=>{const r=e.getBoundingClientRect();return {x:r.x,y:r.y,width:r.width,height:r.height}};const section=name=>[...document.querySelectorAll('main section[aria-labelledby]')].find(s=>document.getElementById(s.getAttribute('aria-labelledby'))?.textContent.startsWith(name));const panel=section('Personas y perfiles');const table=panel.querySelector('table');return {viewport:innerWidth,scrollWidth:document.documentElement.scrollWidth,profiles:rect(panel),equipment:rect(section('Equipamiento')),table:rect(table),rows:panel.querySelectorAll('tbody tr[data-entity-id]').length,headers:[...table.querySelectorAll('th')].filter(e=>e.getBoundingClientRect().width>0).map(e=>e.textContent),firstRowHeight:table.querySelector('tbody tr')?.getBoundingClientRect().height,firstRowLinks:[...table.querySelectorAll('tbody tr:first-child a')].filter(e=>e.getBoundingClientRect().width>0).map(e=>e.textContent),configurationHeight:document.querySelector('#workstation-configurations')?.getBoundingClientRect().height??null};});""")
self.assertLessEqual(geometry['scrollWidth'],geometry['viewport'])
self.save(name, {'image': image, 'geometry': geometry})
print(self.phase+': '+name,flush=True)
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_00_prepare_long_data_and_volume(self):
if self.phase != 'antes':
self.skipTest('Las fixtures se preparan antes de modificar la UI.')
company=self.fixture['companyId']
profile=next(p for p in self.api('/work-profiles/'+company)['profiles'] if p['id']==self.fixture['primaryProfile'])
account=next(p for p in self.api('/access/'+company)['items'] if p['id']==profile['values']['accountId'])
login='angela.munoz.documentacion.administracion.'*3+'example.invalid'
self.api('/access/'+company+'/'+account['id']+'/update',{'values':dict(account['values'],login=login)})
mapping=next(p for p in self.api('/folder-mappings/'+company)['mappings'] if p['values']['profileId']==profile['id'])
path='\\\\srv-documentacion.example.invalid\\administracion\\documentacion-e-infraestructura-documentacion-e-infraestructura-documentacion-e-infraestructura-documentacion-e-infraestructura-documentacion-e-infraestructura-consulta'
need=next(x for x in self.api('/access/'+company)['items'] if x['id']==mapping['values']['needId'])
resource=next(x for x in self.api('/resources/'+company)['resources'] if x['id']==need['values']['resourceId'])
values={key:resource['values'][key] for key in ('name','templateId','active','notes','characteristics','integration')}
values['characteristics']={key:path if value==mapping['values']['path'] else value for key,value in values['characteristics'].items()}
self.api('/resources/'+company+'/'+resource['id']+'/update',{'values':values})
self.api('/folder-mappings/'+company+'/'+mapping['id']+'/update',{'values':dict(mapping['values'],path=path),'expectedRevisionId':mapping['history'][-1]['id']})
self.code('const origin='+json.dumps(self.origin)+',id='+json.dumps(company)+";const {csrfToken}=await(await page.request.get(origin+'/api/auth/session')).json();const before=await(await page.request.get(origin+'/api/assets/'+id)).json();for(let i=0;i<50;i++){const name='UIUX-V21-PUESTO-'+String(i).padStart(2,'0');if(!before.workstations.some(s=>s.values.name===name)){const r=await page.request.post(origin+'/api/assets/'+id+'/workstations',{headers:{origin,'x-csrf-token':csrfToken},data:{values:{name,active:true,siteId:null,personId:null,computerAssetId:null,assetIds:[]}}});if(r.status()!==201)throw Error('Fixture no creada')}}return true;")
self.save('fixtures',{'longLogin':login,'longPath':path,'counts':self.fixture['counts'],'workstations':len(self.api('/assets/'+company)['workstations'])})
def test_01_comparable_captures(self):
for width,height in SIZES:
self.tool('browser_resize',{'width':width,'height':height})
self.navigate()
self.capture('principal-'+str(width))
self.code("await page.getByRole('region',{name:/^Personas y perfiles/}).scrollIntoViewIfNeeded();return true;")
self.capture('perfiles-'+str(width))
for width,height in [(1440,900),(390,844)]:
self.tool('browser_resize',{'width':width,'height':height})
self.navigate();self.expand_primary()
self.code("await page.locator('#workstation-configurations').scrollIntoViewIfNeeded();return true;")
self.capture('configuraciones-'+str(width))
self.code("await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).click();return true;")
self.capture('pendientes-'+str(width))
self.tool('browser_resize',{'width':1440,'height':900});self.navigate('dense')
self.capture('denso-resumen')
self.code("await page.getByRole('region',{name:/^Personas y perfiles/}).getByRole('button',{name:/^Mostrar todos/}).click();return true;")
self.capture('denso-50')
self.code("await page.getByRole('button',{name:'10 perfiles archivados',exact:true}).click();return true;")
self.capture('denso-60-con-archivados')
self.code("await page.getByRole('region',{name:/^Equipamiento/}).getByRole('button',{name:/^Mostrar todos/}).click();return true;")
self.capture('equipamiento-80')
self.tool('browser_resize',{'width':390,'height':844});self.navigate('dense')
self.code("await page.getByRole('region',{name:/^Personas y perfiles/}).scrollIntoViewIfNeeded();return true;")
self.capture('denso-390')
for width,height in [(1440,900),(320,720)]:
self.tool('browser_resize',{'width':width,'height':height});self.navigate('empty');self.capture('vacio-'+str(width))
self.save('pageErrors',self.code('return page.__uiuxErrors;'))
self.assertEqual(self.code('return page.__uiuxErrors;'),[])
def test_02_container_thresholds(self):
self.tool('browser_resize',{'width':1600,'height':1000});self.navigate()
results=[]
for width in [599,600,601,839,840,841]:
result=self.code("const panel=page.getByRole('region',{name:/^Personas y perfiles/});await panel.locator('[data-slot=card]').evaluate((e,w)=>{e.style.width=w+'px'},"+str(width)+");await page.evaluate(()=>new Promise(requestAnimationFrame));return await panel.evaluate(e=>{const card=e.querySelector('[data-slot=card]'),table=e.querySelector('table'),visible=x=>x.getBoundingClientRect().width>0;return {container:card.getBoundingClientRect().width,table:table.getBoundingClientRect().width,scrollWidth:table.parentElement.scrollWidth,headers:[...table.querySelectorAll('th')].filter(visible).map(x=>x.textContent),links:[...table.querySelectorAll('tbody tr:first-child a')].filter(visible).map(x=>x.textContent)}});")
results.append(result)
if self.phase=='despues':
self.assertLessEqual(result['scrollWidth'],result['table']+1)
self.assertEqual(len(result['headers']),2 if width<600 else 4 if width<840 else 5)
self.assertEqual(len(result['links']),3)
self.save('thresholds',results)
def test_03_dense_scope_and_keyboard(self):
self.tool('browser_resize',{'width':1440,'height':900});self.navigate('dense')
result=self.code("""const p=page.getByRole('region',{name:/^Personas y perfiles/}),e=page.getByRole('region',{name:/^Equipamiento/});const count=async x=>(await x.locator('h2').innerText()).trim();const initial={profiles:await count(p),equipment:await count(e),scope:await p.getByRole('status').innerText(),equipmentScope:await e.getByRole('status').innerText()};await p.getByRole('button',{name:'Mostrar todos',exact:true}).click();const rows50=await p.locator('tbody tr[data-entity-id]').count();await p.getByRole('button',{name:'10 perfiles archivados',exact:true}).click();const rows60=await p.locator('tbody tr[data-entity-id]').count();const archivedHeader=await count(p);const expanded=p.getByRole('button',{name:/^Mostrar configuraciones de/}).last();const name=await expanded.getAttribute('aria-label');const initialButton=p.getByRole('button',{name,exact:true});await initialButton.focus();await initialButton.press('Enter');const selected=p.getByRole('button',{name:name.replace('Mostrar','Ocultar'),exact:true});await selected.waitFor();const controls=await selected.getAttribute('aria-controls');const focus=await selected.evaluate(x=>x===document.activeElement);await p.getByRole('button',{name:'Mostrar menos',exact:true}).click();const pinned={rows:await p.locator('tbody tr[data-entity-id]').count(),scope:await p.getByRole('status').innerText(),selectedVisible:await p.getByRole('button',{name:name.replace('Mostrar','Ocultar'),exact:true}).isVisible()};await e.getByRole('button',{name:'Mostrar todos',exact:true}).click();const equipment80=await e.locator('[data-slot=compact-list-item]').count();await e.getByRole('button',{name:'Mostrar menos',exact:true}).click();return {initial,rows50,rows60,archivedHeader,pinned,equipment80,equipment5:await e.locator('[data-slot=compact-list-item]').count(),focus,controlled:await page.locator('[id="'+controls+'"]').isVisible(),principal:await e.locator('[data-slot=compact-list-item]').first().innerText()};""")
self.save('denseInteractions',result)
self.assertTrue(result['initial']['profiles'].endswith('50'));self.assertTrue(result['initial']['equipment'].endswith('80'))
self.assertEqual(result['initial']['scope'],'5 de 50 perfiles activos');self.assertEqual(result['initial']['equipmentScope'],'5 de 80 elementos de equipamiento')
self.assertEqual((result['rows50'],result['rows60'],result['equipment80'],result['equipment5']),(50,60,80,5))
self.assertTrue(result['archivedHeader'].endswith('60'))
self.assertEqual(result['pinned']['rows'],6);self.assertEqual(result['pinned']['scope'],'6 de 60 perfiles, activos y archivados')
self.assertTrue(result['pinned']['selectedVisible']);self.assertTrue(result['focus']);self.assertTrue(result['controlled']);self.assertIn('Principal',result['principal'])
self.save('denseInteractions',result)
def test_04_technical_disclosure_and_pending(self):
self.tool('browser_resize',{'width':390,'height':844});self.navigate();self.expand_primary()
result=self.code("""const configurations=page.locator('#workstation-configurations'),folder=configurations.getByRole('region',{name:'Carpetas del perfil',exact:true});const trigger=folder.getByRole('button',{name:/^Ver detalles técnicos:/});await trigger.waitFor();const closed=await trigger.getAttribute('aria-expanded');await trigger.focus();await trigger.press('Space');const open=folder.getByRole('button',{name:/^Ocultar detalles técnicos:/});const id=await open.getAttribute('aria-controls');const details=page.locator('[id="'+id+'"]');const text=await details.innerText();const focus=await open.evaluate(e=>document.activeElement===e);await open.press('Enter');const closedAgain=await trigger.getAttribute('aria-expanded');const links=await configurations.getByRole('link',{name:'Gestionar',exact:true}).evaluateAll(es=>es.map(e=>e.getAttribute('href')));await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).click();return {closed,openText:text,focus,closedAgain,links,pending:await configurations.innerText(),overflow:await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth)};""")
self.assertEqual(result['closed'],'false');self.assertEqual(result['closedAgain'],'false');self.assertTrue(result['focus']);self.assertFalse(result['overflow'])
self.assertIn('documentacion-e-infraestructura',result['openText']);self.assertEqual(len(result['links']),5)
for link in result['links']:self.assertIn('/'+self.fixture['companyId']+'/work-profiles/'+self.fixture['primaryProfile']+'/',link)
self.assertIn('Instalación pendiente de completar.',result['pending']);self.save('technicalInteractions',result)
def test_05_zoom_reflow_and_dark(self):
results=[]
for zoom in [2,4]:
self.tool('browser_resize',{'width':1440,'height':900});self.navigate();self.expand_primary()
result=self.code("const cdp=await page.context().newCDPSession(page);page.__zoomCdp=cdp;await cdp.send('Emulation.setDeviceMetricsOverride',{width:"+str(1440//zoom)+",height:"+str(900//zoom)+",deviceScaleFactor:"+str(zoom)+",mobile:false});await page.evaluate(()=>new Promise(requestAnimationFrame));return await page.evaluate(()=>({dpr:devicePixelRatio,viewport:innerWidth,scrollWidth:document.documentElement.scrollWidth}));")
self.assertLessEqual(result['scrollWidth'],result['viewport']);self.capture('reflow-'+str(zoom*100));results.append(result)
self.code("await page.__zoomCdp.send('Emulation.clearDeviceMetricsOverride');await page.__zoomCdp.detach();delete page.__zoomCdp;return true;")
self.tool('browser_resize',{'width':390,'height':844});self.navigate();self.expand_primary()
self.code("await page.evaluate(()=>document.documentElement.classList.add('dark'));return true;")
self.capture('oscuro-390')
self.code("await page.evaluate(()=>document.documentElement.classList.remove('dark'));return true;")
self.save('zoomEmulation',results)
def test_06_source_loading_refetch_error_and_recovery(self):
route='**/api/work-profiles/'+self.fixture['companyId']
self.code("await page.route("+json.dumps(route)+",r=>{page.__heldProfiles=r});return true;")
try:
self.tool('browser_navigate',{'url':self.base+'/workstations/'+self.fixture['main']})
result=self.code("const p=page.getByRole('region',{name:/^Personas y perfiles/});await p.locator('table[aria-busy=true]').waitFor();return {countBadges:await p.locator('h2 span.tabular-nums').count(),loading:true};")
self.assertEqual(result['countBadges'],0)
finally:
self.code("if(page.__heldProfiles){await page.__heldProfiles.continue();delete page.__heldProfiles}await page.unroute("+json.dumps(route)+");return true;")
self.code("await page.getByRole('link',{name:'Ángela · administración',exact:true}).waitFor();return true;")
self.code("await page.route("+json.dumps(route)+",r=>r.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));await page.context().setOffline(true);await page.context().setOffline(false);return true;")
try:
stale=self.code("const p=page.getByRole('region',{name:/^Personas y perfiles/});await p.getByRole('alert').waitFor();return {badges:await p.locator('h2 span.tabular-nums').count(),rows:await p.locator('tbody tr[data-entity-id]').count(),message:await p.getByRole('alert').innerText()};")
self.assertEqual(stale['badges'],0);self.assertEqual(stale['rows'],2)
finally:self.code("await page.unroute("+json.dumps(route)+");return true;")
self.code("const p=page.getByRole('region',{name:/^Personas y perfiles/});await p.getByRole('button',{name:'Reintentar',exact:true}).click();await p.getByRole('alert').waitFor({state:'hidden'});await p.locator('h2 span.tabular-nums').waitFor();return true;")
self.save('sourceStates',{'loading':result,'stale':stale,'recovered':True})
def test_07_unknown_review_partial_error_and_empty(self):
route='**/api/folder-mappings/'+self.fixture['companyId']
profile=self.fixture['primaryProfile']
self.code("await page.route("+json.dumps(route)+",async route=>{const r=await route.fetch();const json=await r.json();for(const m of json.mappings){if(m.values.profileId==="+json.dumps(profile)+"){m.values.state='pending';m.needsReview=true;m.accessAccountCoverage='unknown';m.values.notes='Revisar cobertura documental; no comprueba permisos SMB efectivos.'}}await route.fulfill({response:r,json})});return true;")
try:
self.navigate();self.expand_primary()
result=self.code("const f=page.locator('#workstation-configurations').getByRole('region',{name:'Carpetas del perfil',exact:true});const button=f.getByRole('button',{name:/^Ver detalles técnicos:/});await button.waitFor();const closed=await button.getAttribute('aria-expanded');const text=await f.innerText();await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).click();return {closed,text,pendingText:await f.innerText(),focused:await page.locator('#workstation-configurations').evaluate(e=>e===document.activeElement)};")
self.assertEqual(result['closed'],'false');self.assertIn('Cuenta no indicada',result['text']);self.assertIn('Requiere revisión',result['text']);self.assertIn('no comprueba permisos SMB efectivos',result['pendingText']);self.assertTrue(result['focused'])
self.capture('cobertura-desconocida-pendiente')
finally:self.code("await page.unroute("+json.dumps(route)+");return true;")
self.code("await page.route("+json.dumps(route)+",r=>r.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;")
try:
self.tool('browser_navigate',{'url':self.base+'/workstations/'+self.fixture['main']})
self.code("await page.getByText('No se puede confirmar el total de pendientes.',{exact:true}).waitFor();return true;")
self.expand_primary()
self.code("await page.locator('#workstation-configurations').getByText('Instalación pendiente de completar.',{exact:true}).waitFor();return true;")
self.capture('error-parcial-carpetas')
finally:self.code("await page.unroute("+json.dumps(route)+");return true;")
self.navigate('empty')
counts=self.code("return {profiles:await page.getByRole('region',{name:/^Personas y perfiles/}).locator('h2 span.tabular-nums').innerText(),equipment:await page.getByRole('region',{name:/^Equipamiento/}).locator('h2 span.tabular-nums').innerText()};")
self.assertEqual(counts,{'profiles':'0','equipment':'0'});self.save('unknownAndPartial',{'unknown':result,'partialOtherFamiliesAvailable':True,'empty':counts})
def test_08_v1_regressions(self):
# Reutiliza los recorridos de v1, preservando íntegros su código y evidencias.
source=ROOT/'scripts/workstation-uiux-v1.test.py'
code=source.read_text().replace('implementacion-v1"','implementacion-v2.1/regresiones-v1"')
code=code.replace("name:'Equipamiento',exact:true", "name:/^Equipamiento/")
namespace={'__file__':str(source),'__name__':'uiux_v1_regressions'}
exec(compile(code,str(source),'exec'),namespace)
case=namespace['WorkstationUiuxV1Tests']
names=[name for name in unittest.defaultTestLoader.getTestCaseNames(case) if not name.startswith('test_01')]
result=unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite(case(name) for name in names))
self.save('v1Regressions',{'run':result.testsRun,'failures':len(result.failures),'errors':len(result.errors),'selectorsAdapted':'Equipamiento ahora incluye el contador en su nombre accesible.'})
self.assertTrue(result.wasSuccessful())
def test_09_totals_change_during_refetch(self):
company=self.fixture['companyId'];station=next(w for w in self.api('/assets/'+company)['workstations'] if w['id']==self.fixture['dense']);asset=station['values']['computerAssetId']
route='**/api/work-profiles/'+company
self.code("page.__v21Limit=6;await page.route("+json.dumps(route)+",async route=>{const r=await route.fetch();const json=await r.json();json.profiles=json.profiles.filter(p=>p.values.active&&p.values.assetId==="+json.dumps(asset)+").slice(0,page.__v21Limit);await route.fulfill({response:r,json})});return true;")
results=[]
try:
self.navigate('dense')
self.code("await page.getByRole('region',{name:/^Personas y perfiles/}).getByRole('button',{name:'Mostrar todos',exact:true}).click();return true;")
for total in [5,1,0,50]:
result=self.code("page.__v21Limit="+str(total)+";await page.context().setOffline(true);await page.context().setOffline(false);const p=page.getByRole('region',{name:/^Personas y perfiles/});await p.locator('h2 span.tabular-nums').filter({hasText:/^"+str(total)+"$/}).waitFor();return {total:"+str(total)+",rows:await p.locator('tbody tr[data-entity-id]').count(),buttons:await p.getByRole('button',{name:/^Mostrar (todos|menos)/}).count()};")
self.assertEqual(result['rows'],total);self.assertEqual(result['buttons'],1 if total>5 else 0);results.append(result)
finally:self.code("await page.unroute("+json.dumps(route)+");delete page.__v21Limit;return true;")
self.save('refetchTotals',results)
def test_10_profile_space_full_values_and_refetch(self):
self.tool('browser_resize',{'width':390,'height':844});self.navigate()
result=self.code("""const name='Ángela · administración';const button=page.getByRole('button',{name:'Mostrar configuraciones de '+name,exact:true});await button.focus();await button.press('Space');const selected=page.getByRole('button',{name:'Ocultar configuraciones de '+name,exact:true});await selected.waitFor();const profileFocused=await selected.evaluate(e=>e===document.activeElement);const focusVisible=await selected.evaluate(e=>e.matches(':focus-visible'));await selected.press('Tab');const next=await page.evaluate(()=>document.activeElement?.getAttribute('href'));const folder=page.locator('#workstation-configurations').getByRole('region',{name:'Carpetas del perfil',exact:true});await folder.getByRole('button',{name:/^Ver detalles técnicos:/}).press('Space');const technical=folder.getByRole('button',{name:/^Ocultar detalles técnicos:/});const values=await folder.locator('dd').allTextContents();const response=page.waitForResponse(r=>r.url().includes('/api/work-profiles/')&&r.status()===200);await page.context().setOffline(true);await page.context().setOffline(false);await response;await page.evaluate(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))));return {profileFocused,focusVisible,next,values,profileExpanded:await selected.getAttribute('aria-expanded'),technicalExpanded:await technical.getAttribute('aria-expanded'),technicalFocused:await technical.evaluate(e=>e===document.activeElement)};""")
self.assertTrue(result['profileFocused']);self.assertTrue(result['focusVisible']);self.assertTrue(result['technicalFocused']);self.assertEqual(result['profileExpanded'],'true');self.assertEqual(result['technicalExpanded'],'true')
self.assertIn('/work-profiles/'+self.fixture['secondaryProfile'],result['next'])
self.assertIn(self.measurements['antes']['fixtures']['longPath'],result['values'])
login=self.measurements['antes']['fixtures']['longLogin'];self.assertTrue(any(login in value for value in result['values']))
self.save('spaceFullValuesAndRefetch',result)
self.code("await page.locator('#workstation-configurations').scrollIntoViewIfNeeded();return true;")
self.capture('detalles-tecnicos-390')
self.code("await page.evaluate(()=>document.documentElement.classList.add('dark'));return true;")
self.capture('configuraciones-oscuras-390')
self.code("await page.evaluate(()=>document.documentElement.classList.remove('dark'));return true;")
self.tool('browser_resize',{'width':1440,'height':900})
self.code("await page.locator('#workstation-configurations').scrollIntoViewIfNeeded();return true;")
self.capture('detalles-tecnicos-1440')
def test_11_entity_family_navigation_and_isolation(self):
self.tool('browser_resize',{'width':1440,'height':900});self.navigate()
targets=[self.base+'/assets/'+self.fixture['host'],self.base+'/work-profiles/'+self.fixture['primaryProfile']]
person=self.code("""return await page.getByRole('region',{name:/^Personas y perfiles/}).locator('tbody tr').first().locator('a[href*="/people/"]:visible').getAttribute('href');""")
targets.append(self.origin+person if person.startswith('/') else person)
visited=[]
for target in targets:
self.code("await page.locator('main a[href="+json.dumps(target.replace(self.origin,''))+ "]:visible').first().click();await page.waitForURL("+json.dumps(target)+");await page.locator('main h1, main h2').filter({hasText:/PORT-ADM-01|Ángela/}).first().waitFor();return true;")
visited.append(target.replace(self.origin,''))
self.code("await page.goBack();await page.waitForURL("+json.dumps(self.base+'/workstations/'+self.fixture['main'])+");return true;")
titles={'folder-mappings':'Carpetas del perfil','mail-configurations':'Correos en dispositivos','application-configurations':'Aplicaciones del perfil','printer-configurations':'Impresoras del perfil','certificate-configurations':'Certificados del perfil'}
for family,title in titles.items():
self.navigate();self.expand_primary();target=self.base+'/work-profiles/'+self.fixture['primaryProfile']+'/'+family
self.code("await page.locator('#workstation-configurations').locator('a[href="+json.dumps(target.replace(self.origin,''))+"]').click();await page.waitForURL("+json.dumps(target)+");await page.locator('#workstation-configurations').waitFor({state:'hidden'});await page.getByRole('heading',{name:"+json.dumps(title)+",exact:true}).waitFor();return true;")
visited.append(target.replace(self.origin,''))
other=self.origin+'/companies/'+self.fixture['otherCompany']+'/workstations/'+self.fixture['main']
self.tool('browser_navigate',{'url':other})
self.code("await page.getByText('Este puesto de trabajo no existe en esta compañía.',{exact:true}).waitFor();return true;")
self.assertFalse(self.code("return await page.getByRole('link',{name:'Ángela · administración',exact:true}).isVisible();"))
status=self.code("const c=await page.context().browser().newContext();try{const r=await c.request.get("+json.dumps(self.origin+'/api/assets/'+self.fixture['companyId'])+");return r.status()}finally{await c.close()}")
self.assertEqual(status,401);self.save('navigationAndIsolation',{'visited':visited,'wrongCompanyWorkstationAbsent':True,'unauthenticatedStatus':status})
def test_12_narrow_header_actions(self):
results=[]
for width,height in [(320,720),(390,844)]:
self.tool('browser_resize',{'width':width,'height':height});self.navigate('dense')
value=self.code("const p=page.getByRole('region',{name:/^Personas y perfiles/});return await p.evaluate(e=>{const card=e.querySelector('[data-slot=card]').getBoundingClientRect();return {viewport:innerWidth,right:card.right-16,actions:[...e.querySelectorAll('[data-slot=card-header] button')].map(b=>{const r=b.getBoundingClientRect();return {name:b.textContent,x:r.x,right:r.right,y:r.y,width:r.width}})}});")
for action in value['actions']:self.assertLessEqual(action['right'],value['right']+1)
results.append(value)
self.save('narrowHeaderActions',results)
def test_13_configuration_state_priority(self):
results=[]
for width,height in [(1440,900),(390,844)]:
self.tool('browser_resize',{'width':width,'height':height});self.navigate();self.expand_primary()
self.code("await page.locator('#workstation-configurations').scrollIntoViewIfNeeded();return true;")
self.capture('configuraciones-'+str(width))
value=self.code("const item=page.locator('#workstation-configurations').getByRole('region',{name:'Carpetas del perfil',exact:true}).locator('[data-slot=compact-list-item]');const trigger=item.getByRole('button',{name:/^Ver detalles técnicos:/});await trigger.click();return await item.evaluate(e=>{const title=e.firstElementChild.firstElementChild,badge=e.querySelector('[data-slot=badge]'),details=e.querySelector('dl');return {viewport:innerWidth,titleY:title.getBoundingClientRect().top,badgeY:badge.getBoundingClientRect().top,detailsY:details.getBoundingClientRect().top}});")
self.assertAlmostEqual(value['titleY'],value['badgeY'],delta=2);self.assertLess(value['badgeY'],value['detailsY']);results.append(value)
self.code("await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).click();return true;")
self.capture('pendientes-'+str(width))
self.save('configurationStatePriority',results)
if __name__ == '__main__':
unittest.main()