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