2026-10-21 17:31:09 +00:00
#!/usr/bin/env python3
""" Regresión UI real sobre la sesión sintética del preparador, mediante su cliente MCP.
METIS_PLAYWRIGHT_STORAGE_STATE = . artifacts / playwright / authenticated - … / session . json \
METIS_UI_AUDIT_PHASE = after python3 scripts / visual - audit - ui . test . py
No prepara datos , no inicia servicios y bloquea escrituras HTTP en el navegador .
"""
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 = [ ( 1440 , 900 ) , ( 1366 , 768 ) , ( 390 , 844 ) , ( 320 , 640 ) ]
class VisualUiTests ( unittest . TestCase ) :
@classmethod
def setUpClass ( cls ) :
state_value = os . environ . get ( " METIS_PLAYWRIGHT_STORAGE_STATE " )
if not state_value :
raise unittest . SkipTest ( " Requiere una sesión sintética activa de scripts/visual-audit.py. " )
state = ( ROOT / state_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 estar dentro de .artifacts/playwright. " )
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 el informe del entorno sintético local. " )
phase = os . environ . get ( " METIS_UI_AUDIT_PHASE " , " after " )
if phase not in ( " before " , " after " ) :
raise ValueError ( " Fase inválida. " )
2026-10-21 20:55:13 +00:00
output_value = os . environ . get ( " METIS_UI_AUDIT_DIRECTORY " )
cls . output = ( ROOT / output_value ) . resolve ( ) if output_value else ROOT / " .artifacts/playwright/ui-sprint-1 " / phase
if not cls . output . is_relative_to ( ROOT / " .artifacts/playwright " ) :
raise RuntimeError ( " Las evidencias deben quedar dentro de .artifacts/playwright. " )
2026-10-21 17:31:09 +00:00
cls . output . mkdir ( parents = True , exist_ok = True )
cls . origin = report [ " origin " ]
cls . company = " /companies/ " + report [ " companyId " ]
cls . results = json . loads ( ( cls . output / " measurements.json " ) . read_text ( ) ) if ( cls . output / " measurements.json " ) . exists ( ) else [ ]
config = tomllib . loads ( ( ROOT / " .codex/config.toml " ) . read_text ( ) ) [ " mcp_servers " ] [ " playwright " ]
cls . stack = contextlib . ExitStack ( )
cls . addClassCleanup ( cls . stack . close )
cls . client = cls . stack . enter_context ( AUDIT [ " browser " ] ( config , state ) )
cls . tool ( " browser_resize " , { " width " : 1440 , " height " : 900 } )
cls . code ( " await page.route( ' **/api/** ' , route => [ ' GET ' , ' HEAD ' , ' OPTIONS ' ].includes(route.request().method()) ? route.continue() : route.abort()); return true; " )
2026-10-21 20:55:13 +00:00
@classmethod
def tearDownClass ( cls ) :
if hasattr ( cls , " client " ) :
console = cls . tool ( " browser_console_messages " , { " level " : " error " } )
( cls . output / " console.json " ) . write_text ( json . dumps ( console , ensure_ascii = False , indent = 2 ) + " \n " )
2026-10-21 17:31:09 +00:00
@classmethod
def tool ( cls , name , arguments ) :
return AUDIT [ " tool " ] ( cls . client , name , arguments )
@classmethod
def code ( cls , body ) :
return AUDIT [ " result_json " ] ( cls . tool ( " browser_run_code_unsafe " , { " code " : " async (page) => { " + body + " } " } ) )
@classmethod
def record ( cls , name , value ) :
cls . results . append ( { " scenario " : name , " value " : value } )
( cls . output / " measurements.json " ) . write_text ( json . dumps ( cls . results , ensure_ascii = False , indent = 2 ) + " \n " )
def navigate ( self , section ) :
self . tool ( " browser_navigate " , { " url " : self . origin + self . company + " / " + section } )
self . code ( " await page.locator( ' [data-slot=workspace] ' ).waitFor(); return true; " )
def capture ( self , name ) :
AUDIT [ " capture " ] ( self . client , self . output , name )
result = self . tool ( " browser_snapshot " , { } )
( self . output / ( name + " .txt " ) ) . write_text ( " \n " . join ( p [ " text " ] for p in result [ " content " ] if p . get ( " type " ) == " text " ) )
def resize ( self , width , height ) :
self . tool ( " browser_resize " , { " width " : width , " height " : height } )
self . settle ( )
def settle ( self ) :
self . code ( " await page.evaluate(async()=> { await document.fonts.ready; await Promise.all(document.getAnimations().filter(a=>a.effect?.getTiming().iterations!==Infinity).map(a=>a.finished.catch(()=> {} )))}); return true; " )
def test_selected_row_contrast ( self ) :
self . resize ( 1440 , 900 )
self . navigate ( " assets " )
self . code ( " await page.getByRole( ' link ' , { name: ' AUDIT-PORT-01 ' ,exact:true}).waitFor();return true; " )
values = [ ]
for state in ( " normal " , " normal-hover " , " normal-focus " , " selected " , " hover " , " focus " ) :
if state == " selected " :
self . code ( " await page.getByRole( ' row ' ).filter( { has:page.getByRole( ' link ' , { name: ' AUDIT-PORT-01 ' ,exact:true})}).locator( ' td ' ).nth(2).click();return true; " )
if state . endswith ( " hover " ) :
self . code ( " await page.getByRole( ' link ' , { name: ' AUDIT-PORT-01 ' ,exact:true}).hover(); return true; " )
if state . endswith ( " focus " ) :
self . code ( " await page.mouse.move(0,0); await page.getByRole( ' link ' , { name: ' AUDIT-PORT-01 ' ,exact:true}).focus(); await page.keyboard.press( ' Tab ' ); await page.keyboard.press( ' Shift+Tab ' ); return true; " )
self . settle ( )
measurements = self . code ( """ return await page.evaluate(()=> {
const rgb = c = > { const ctx = document . createElement ( ' canvas ' ) . getContext ( ' 2d ' ) ; ctx . fillStyle = c ; ctx . fillRect ( 0 , 0 , 1 , 1 ) ; return [ . . . ctx . getImageData ( 0 , 0 , 1 , 1 ) . data ] . slice ( 0 , 3 ) } ;
const background = e = > { const colors = [ ] ; for ( let n = e ; n ; n = n . parentElement ) colors . unshift ( getComputedStyle ( n ) . backgroundColor ) ; const ctx = document . createElement ( ' canvas ' ) . getContext ( ' 2d ' ) ; ctx . fillStyle = ' #fff ' ; ctx . fillRect ( 0 , 0 , 1 , 1 ) ; for ( const color of colors ) { ctx . fillStyle = color ; ctx . fillRect ( 0 , 0 , 1 , 1 ) } return [ . . . ctx . getImageData ( 0 , 0 , 1 , 1 ) . data ] . slice ( 0 , 3 ) } ;
const lum = c = > c . map ( v = > v / 255 ) . map ( v = > v < = .04045 ? v / 12.92 : ( ( v + .055 ) / 1.055 ) * * 2.4 ) . reduce ( ( s , v , i ) = > s + v * [ .2126 , .7152 , .0722 ] [ i ] , 0 ) ;
const row = [ . . . document . querySelectorAll ( ' tr ' ) ] . find ( e = > e . textContent . includes ( ' AUDIT-PORT-01 ' ) ) ;
return [ . . . row . querySelectorAll ( ' a,button,[class*=text-muted-foreground] ' ) ] . map ( e = > { const fg = rgb ( getComputedStyle ( e ) . color ) ; const bg = background ( e ) ; const l = [ lum ( fg ) , lum ( bg ) ] . sort ( ( a , b ) = > a - b ) ; return { label : e . textContent . trim ( ) | | e . getAttribute ( ' aria-label ' ) , fg , bg , ratio : ( l [ 1 ] + .05 ) / ( l [ 0 ] + .05 ) } ; } ) ;
} ) ;
""" )
if state . endswith ( " focus " ) :
self . assertTrue ( self . code ( " return await page.getByRole( ' link ' , { name: ' AUDIT-PORT-01 ' ,exact:true}).evaluate(e=>e.matches( ' :focus-visible ' )); " ) )
values . extend ( measurements )
self . record ( " contrast- " + state , measurements )
self . capture ( " contrast- " + state + " -1440 " )
self . assertTrue ( all ( x [ " ratio " ] > = 4.5 for x in values ) , values )
def test_wizard_geometry ( self ) :
failures = [ ]
for section , title , field , template in [ ( " assets " , " Nuevo activo " , " Plantilla " , " Portátil " ) , ( " resources " , " Nuevo recurso " , " Plantilla de recurso " , " Aplicación " ) ] :
for width , height in SIZES :
self . resize ( width , height )
self . navigate ( section )
self . code ( " await page.getByRole( ' button ' , { name: " + json . dumps ( title ) + " ,exact:true}).click(); await page.getByRole( ' dialog ' ).getByRole( ' combobox ' , { name: " + json . dumps ( field ) + " ,exact:true}).click(); await page.getByRole( ' option ' , { name: " + json . dumps ( template ) + " ,exact:true}).click(); return true; " )
# Tras operar el desplegable, inspecciona el inicio del cuerpo desplazable.
self . code ( " await page.locator( ' [role=dialog] [data-slot=form-body] ' ).evaluate(e=>e.scrollTo( {top:0} ));return true; " )
geometry = [ ]
for step in ( 1 , 2 , 3 ) :
self . settle ( )
value = self . code ( """ return await page.evaluate(()=> { const d=document.querySelector( ' [role=dialog] ' );const r=d.getBoundingClientRect();const f=d.querySelector( ' [data-slot=dialog-footer] ' ).getBoundingClientRect();const b=d.querySelector( ' [data-slot=form-body] ' );return { y:r.y,height:r.height,bottom:r.bottom,footerY:f.y,footerBottom:f.bottom,progressY:b.querySelector( ' ol ' ).getBoundingClientRect().y,bodyClient:b.clientHeight,bodyScroll:b.scrollHeight};}); """ )
geometry . append ( value )
self . capture ( f " { section } - { width } -step- { step } " )
if step < 3 :
self . code ( " await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Siguiente ' ,exact:true}).click(); return true; " )
self . record ( f " geometry- { section } - { width } " , geometry )
if max ( x [ " height " ] for x in geometry ) - min ( x [ " height " ] for x in geometry ) > 1 or max ( x [ " footerY " ] for x in geometry ) - min ( x [ " footerY " ] for x in geometry ) > 1 :
failures . append ( f " { section } { width } : altura o pie variables " )
if max ( x [ " progressY " ] for x in geometry ) - min ( x [ " progressY " ] for x in geometry ) > 1 :
failures . append ( f " { section } { width } : progreso desplazado " )
if any ( x [ " y " ] < 0 or x [ " bottom " ] > height + 1 or x [ " footerBottom " ] > height + 1 for x in geometry ) :
failures . append ( f " { section } { width } : fuera de viewport " )
self . code ( " await page.keyboard.press( ' Escape ' ); await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' }); return true; " )
self . assertEqual ( failures , [ ] )
def test_focus_return ( self ) :
self . resize ( 1440 , 900 )
failures = [ ]
for origin , opening , closing in [ ( " header " , " mouse " , " Escape " ) , ( " header " , " keyboard " , " Escape " ) , ( " header " , " mouse " , " Cancelar " ) , ( " row " , " keyboard " , " Escape " ) , ( " row " , " mouse " , " Cancelar " ) , ( " missing " , " keyboard " , " Escape " ) , ( " disabled " , " mouse " , " Escape " ) ] :
self . navigate ( " assets " )
label = " Editar " if origin == " row " else " Nuevo activo "
self . code ( " const trigger=page.getByRole( ' button ' , { name: " + json . dumps ( label ) + " ,exact:true}); await trigger.waitFor(); " + ( " await trigger.focus();await page.keyboard.press( ' Enter ' ); " if opening == " keyboard " else " await trigger.click(); " ) + " await page.getByRole( ' dialog ' ).waitFor();return true; " )
if origin == " missing " :
self . code ( " await page.locator( ' [data-slot=inventory-layout] button ' ).filter( { hasText: ' Nuevo activo ' }).evaluate(e=>e.remove());return true; " )
if origin == " disabled " :
self . code ( " await page.locator( ' [data-slot=inventory-layout] button ' ).filter( { hasText: ' Nuevo activo ' }).evaluate(e=>e.disabled=true);return true; " )
self . code ( ( " await page.keyboard.press( ' Escape ' ); " if closing == " Escape " else " await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Cancelar ' ,exact:true}).click(); " ) + " await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });return true; " )
self . settle ( )
result = self . code ( " await page.waitForFunction(()=>document.activeElement!==document.body, {} , {timeout:2000} ).catch(()=> {} );return await page.evaluate(()=>( { tag:document.activeElement.tagName,label:document.activeElement.getAttribute( ' aria-label ' )||document.activeElement.textContent.trim().slice(0,80),search:document.activeElement.hasAttribute( ' data-quick-search ' )})); " )
self . record ( f " focus- { origin } - { opening } - { closing } " , result )
self . capture ( f " focus- { origin } - { opening } - { closing } " )
if ( origin in ( " missing " , " disabled " ) and not result [ " search " ] ) or ( origin not in ( " missing " , " disabled " ) and result [ " label " ] != label ) :
failures . append ( [ origin , opening , closing , result ] )
for width , height in SIZES [ 1 : ] :
self . resize ( width , height )
self . navigate ( " assets " )
self . code ( " await page.getByRole( ' button ' , { name: ' Nuevo activo ' ,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 . settle ( )
target = self . code ( " return await page.evaluate(()=>document.activeElement.textContent.trim()); " )
self . record ( f " focus-header- { width } " , target )
self . capture ( f " focus-header- { width } " )
self . assertEqual ( target , " Nuevo activo " )
self . resize ( 1440 , 900 )
self . navigate ( " work-profiles " )
self . code ( " await page.getByRole( ' button ' , { name: ' Nuevo perfil ' ,exact:true}).click(); await page.getByRole( ' button ' , { name: ' Seleccionar puesto ' ,exact:true}).click();return true; " )
self . assertTrue ( self . code ( " return await page.evaluate(()=>document.activeElement.hasAttribute( ' data-quick-search ' )); " ) )
self . code ( " await page.keyboard.press( ' Escape ' );return true; " )
self . settle ( )
self . assertEqual ( self . code ( " return await page.evaluate(()=>document.activeElement.textContent.trim()); " ) , " Seleccionar puesto " )
self . code ( " await page.keyboard.press( ' Escape ' );return true; " )
self . assertEqual ( failures , [ ] )
def test_review_headers ( self ) :
failures = [ ]
self . resize ( 1440 , 900 )
for section in ( " work-profiles " , " folder-mappings " , " application-configurations " , " certificate-configurations " , " printer-configurations " , " mail-configurations " ) :
self . navigate ( section )
self . code ( " await page.getByRole( ' table ' ).waitFor();return true; " )
headers = self . code ( " return await page.locator( ' th ' ).allTextContents(); " )
self . record ( " headers- " + section , headers )
self . code ( " const header=page.getByRole( ' columnheader ' , { name:/Revisión/});if(await header.count())await header.scrollIntoViewIfNeeded();return true; " )
self . capture ( " headers- " + section + " -1440 " )
if not any ( x . strip ( ) . startswith ( " Revisión " ) for x in headers ) or any ( " Revisar: " in x for x in headers ) :
failures . append ( section )
for width , height in SIZES [ 1 : ] :
self . resize ( width , height )
self . navigate ( " work-profiles " )
self . code ( " await page.getByRole( ' table ' ).waitFor();return true; " )
headers = self . code ( " return await page.locator( ' th ' ).allTextContents(); " )
self . record ( f " headers-work-profiles- { width } " , headers )
self . code ( " await page.getByRole( ' columnheader ' , { name:/Revisión/}).scrollIntoViewIfNeeded();return true; " )
self . capture ( f " headers-work-profiles- { width } " )
self . assertTrue ( any ( x . strip ( ) . startswith ( " Revisión " ) for x in headers ) )
self . assertEqual ( failures , [ ] )
2026-10-21 20:55:13 +00:00
def test_mobile_navigation_geometry ( self ) :
baseline_path = ROOT / " .artifacts/playwright/v01-1/before/measurements.json "
baseline = json . loads ( baseline_path . read_text ( ) ) if baseline_path . exists ( ) else [ ]
for width , height in [ ( 320 , 640 ) , ( 390 , 844 ) , ( 768 , 1024 ) , ( 1024 , 768 ) , ( 1366 , 768 ) , ( 1440 , 900 ) , ( 1920 , 1080 ) ] :
self . resize ( width , height )
self . navigate ( " assets " )
self . code ( " await page.getByRole( ' button ' , { name: ' Nuevo activo ' ,exact:true}).waitFor();return true; " )
metrics = self . code ( """ return await page.evaluate(()=> {
const r = e = > { const b = e . getBoundingClientRect ( ) ; return { x : b . x , y : b . y , width : b . width , height : b . height } } ;
return { main : r ( document . querySelector ( ' main ' ) ) , header : r ( document . querySelector ( ' header ' ) ) , global : r ( document . querySelector ( ' [data-workspace-desktop-navigation] ' ) ) , context : r ( document . querySelector ( ' [data-slot=company-context-bar] ' ) ) , overflow : document . documentElement . scrollWidth > innerWidth , dialog : document . querySelectorAll ( ' [role=dialog] ' ) . length , visibleSections : [ . . . document . querySelectorAll ( ' nav[aria-label= " Información de la compañía " ] a ' ) ] . filter ( e = > e . getClientRects ( ) . length > 0 ) . length } ;
} ) ; """ )
self . record ( " navigation-geometry- " + str ( width ) , metrics )
self . capture ( " navigation-assets- " + str ( width ) )
self . assertFalse ( metrics [ " overflow " ] )
self . assertEqual ( metrics [ " dialog " ] , 0 )
if width < 768 :
self . assertLessEqual ( metrics [ " main " ] [ " y " ] , 128 )
self . assertEqual ( metrics [ " global " ] [ " width " ] , 0 )
self . assertEqual ( metrics [ " visibleSections " ] , 0 )
else :
self . assertEqual ( metrics [ " global " ] [ " width " ] , 228 )
self . assertEqual ( metrics [ " context " ] [ " width " ] , 0 )
self . assertEqual ( metrics [ " visibleSections " ] , 21 )
old = next ( ( x [ " metrics " ] [ " main " ] for x in baseline if x [ " screen " ] == " assets " and x [ " width " ] == width ) , None )
if old :
self . assertAlmostEqual ( metrics [ " main " ] [ " width " ] , old [ " w " ] , delta = 1 )
self . assertAlmostEqual ( metrics [ " main " ] [ " y " ] , old [ " y " ] , delta = 1 )
def test_mobile_navigation_keyboard_and_routes ( self ) :
self . resize ( 390 , 844 )
self . navigate ( " assets " )
self . code ( " await page.getByRole( ' button ' , { name:/^Secciones de /}).click();await page.getByRole( ' dialog ' ).waitFor();return true; " )
links = self . code ( " return await page.getByRole( ' dialog ' ).locator( ' nav a ' ).evaluateAll(es=>es.map(e=>( { href:e.getAttribute( ' href ' ),label:e.textContent.trim(),current:e.getAttribute( ' aria-current ' )}))); " )
self . record ( " navigation-21-destinations " , links )
self . assertEqual ( len ( links ) , 21 )
self . assertEqual ( len ( { x [ ' href ' ] for x in links } ) , 21 )
self . assertEqual ( sum ( x [ ' current ' ] == ' page ' for x in links ) , 1 )
self . capture ( " navigation-company-menu-390 " )
focused = self . code ( """ await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Cerrar navegación ' }).focus();const visited=[];for(let i=0;i<26;i++) { await page.keyboard.press( ' Tab ' );await page.waitForFunction(()=>!!document.activeElement.closest( ' [role=dialog] ' ));visited.push(await page.evaluate(()=>( { inside:!!document.activeElement.closest( ' [role=dialog] ' ),href:document.activeElement.getAttribute( ' href ' )})))}return visited; """ )
self . assertTrue ( all ( x [ ' inside ' ] for x in focused ) )
self . assertTrue ( any ( x [ ' href ' ] == links [ - 1 ] [ ' href ' ] for x in focused ) )
reverse = self . code ( """ await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Cerrar navegación ' }).focus();const visited=[];for(let i=0;i<26;i++) { await page.keyboard.press( ' Shift+Tab ' );await page.waitForFunction(()=>!!document.activeElement.closest( ' [role=dialog] ' ));visited.push(await page.evaluate(()=>( { inside:!!document.activeElement.closest( ' [role=dialog] ' ),href:document.activeElement.getAttribute( ' href ' )})))}return visited; """ )
self . record ( " navigation-keyboard-cycle " , { " forward " : focused , " reverse " : reverse } )
self . assertTrue ( all ( x [ ' inside ' ] for x in reverse ) )
self . assertTrue ( any ( x [ ' href ' ] == links [ - 1 ] [ ' href ' ] for x in reverse ) )
self . code ( " await page.getByRole( ' dialog ' ).getByRole( ' link ' , { name:/^Personas/}).focus();await page.keyboard.press( ' Space ' );return true; " )
self . assertTrue ( self . code ( " return page.url().endsWith( ' /assets ' ); " ) )
self . code ( " await page.keyboard.press( ' Enter ' );await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });return true; " )
self . assertTrue ( self . code ( " return page.url().endsWith( ' /people ' ); " ) )
self . code ( " await page.waitForFunction(()=>!!document.activeElement.closest( ' main ' ));return true; " )
self . assertTrue ( self . code ( " return await page.evaluate(()=>document.activeElement.textContent.includes( ' Personas ' )); " ) )
self . code ( " await page.goBack();await page.getByRole( ' button ' , { name: ' Nuevo activo ' ,exact:true}).waitFor();await page.goForward();await page.getByRole( ' button ' , { name: ' Nueva persona ' ,exact:true}).waitFor();return true; " )
self . assertTrue ( self . code ( " return page.url().endsWith( ' /people ' ); " ) )
self . navigate ( " assets " )
self . code ( " await page.getByRole( ' button ' , { name:/^Secciones de /}).click();await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Cambiar compañía ' }).click();return true; " )
switched = self . code ( """ await page.waitForFunction(()=>document.querySelector( ' [role=dialog] ' )?.textContent.includes( ' Menú global ' )&&!!document.activeElement.closest( ' [role=dialog] ' ));return await page.evaluate(()=>( { dialogs:document.querySelectorAll( ' [role=dialog] ' ).length,focusInside:!!document.activeElement.closest( ' [role=dialog] ' )})); """ )
self . record ( " navigation-menu-transfer " , switched )
self . assertEqual ( switched , { " dialogs " : 1 , " focusInside " : True } )
self . capture ( " navigation-global-menu-390 " )
self . assertTrue ( self . code ( " return await page.getByRole( ' dialog ' ).getByRole( ' link ' , { name: ' Inicio ' ,exact:true}).evaluate(e=> { const r=e.getBoundingClientRect();return r.top>=0&&r.bottom<=innerHeight;}); " ) )
self . code ( " await page.keyboard.press( ' Escape ' );await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });return true; " )
self . code ( " await page.waitForFunction(()=>document.activeElement.getAttribute( ' aria-label ' )?.startsWith( ' Secciones de ' ));return true; " )
for button in [ " Menú global " , " Buscar en Metis Desk " ] :
self . code ( " await page.getByRole( ' button ' , { name: " + json . dumps ( button ) + " ,exact:true}).focus();await page.keyboard.press( ' Space ' );await page.getByRole( ' dialog ' ).waitFor();await page.keyboard.press( ' Escape ' );await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });return true; " )
self . code ( " await page.waitForFunction(label=>document.activeElement.getAttribute( ' aria-label ' )===label, " + json . dumps ( button ) + " );return true; " )
self . code ( " await page.getByRole( ' button ' , { name:/^Secciones de /}).click();return true; " )
for click in [ { " modifiers " : [ " Control " ] } , { " button " : " middle " } ] :
result = self . code ( """ const popup=page.context().waitForEvent( ' page ' );await page.getByRole( ' dialog ' ).getByRole( ' link ' , { name:/^Personas/}).click( """ + json . dumps ( click ) + """ );const other=await popup;await other.waitForLoadState( ' domcontentloaded ' );const result= { path:new URL(other.url()).pathname,original:new URL(page.url()).pathname,stillOpen:await page.getByRole( ' dialog ' ).isVisible()};await other.close();return result; """ )
self . record ( " navigation-modified-click- " + str ( click ) , result )
self . assertTrue ( result [ ' path ' ] . endswith ( ' /people ' ) )
self . assertTrue ( result [ ' original ' ] . endswith ( ' /assets ' ) )
self . assertTrue ( result [ ' stillOpen ' ] )
self . code ( " await page.keyboard.press( ' Escape ' );return true; " )
def test_mobile_navigation_search_sticky_and_resize ( self ) :
self . resize ( 320 , 640 )
self . navigate ( " assets " )
self . code ( " await page.keyboard.press( ' Tab ' );await page.getByRole( ' link ' , { name: ' Saltar al contenido ' }).focus();await page.keyboard.press( ' Enter ' );return true; " )
self . assertTrue ( self . code ( " return await page.evaluate(()=>document.activeElement.id=== ' workspace-main ' ); " ) )
self . code ( " await page.getByRole( ' button ' , { name: ' Buscar en Metis Desk ' ,exact:true}).click();await page.getByRole( ' dialog ' ).getByRole( ' textbox ' ).fill( ' Auditoría ' );return true; " )
self . assertEqual ( self . code ( " return await page.getByRole( ' dialog ' ).getByRole( ' link ' ).count(); " ) , 1 )
self . capture ( " navigation-search-320 " )
self . code ( " await page.getByRole( ' dialog ' ).getByRole( ' link ' ).click();await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });return true; " )
self . assertEqual ( self . code ( " return new URL(page.url()).pathname; " ) , self . company )
self . navigate ( " assets " )
self . code ( " await page.getByRole( ' link ' , { name: ' AUDIT-PORT-01 ' ,exact:true}).click();await page.locator( ' main h1 ' ).filter( { hasText: ' AUDIT-PORT-01 ' }).waitFor();await page.locator( ' [data-slot=workspace-scroll] ' ).evaluate(e=>e.scrollTop=800);return true; " )
self . settle ( )
sticky = self . code ( """ return await page.evaluate(()=>( { header:document.querySelector( ' header ' ).getBoundingClientRect().y,context:document.querySelector( ' [data-slot=company-context-bar] ' ).getBoundingClientRect().y,scroll:document.querySelector( ' [data-slot=workspace-scroll] ' ).scrollTop})); """ )
self . record ( " navigation-sticky-320 " , sticky )
self . assertGreater ( sticky [ ' scroll ' ] , 0 )
self . assertEqual ( sticky [ ' header ' ] , 0 )
self . assertAlmostEqual ( sticky [ ' context ' ] , 56 , delta = 2 )
self . capture ( " navigation-sticky-320 " )
self . code ( " await page.locator( ' main button ' ).first().focus();return true; " )
position = self . code ( " return await page.evaluate(()=>document.activeElement.getBoundingClientRect().top); " )
self . assertGreaterEqual ( position , 104 )
self . navigate ( " assets " )
self . code ( " await page.getByRole( ' button ' , { name: ' Menú global ' ,exact:true}).click();return true; " )
self . resize ( 768 , 1024 )
self . code ( " await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });return true; " )
self . assertTrue ( self . code ( " return await page.evaluate(()=>document.activeElement!==document.body&&document.activeElement.getClientRects().length>0); " ) )
self . resize ( 390 , 844 )
self . assertEqual ( self . code ( " return await page.getByRole( ' dialog ' ).count(); " ) , 0 )
self . tool ( ' browser_navigate ' , { ' url ' : self . origin + ' /dashboard ' } )
self . code ( " await page.locator( ' main ' ).waitFor();return true; " )
self . assertEqual ( self . code ( " return await page.locator( ' [data-slot=company-context-bar] ' ).count(); " ) , 0 )
self . code ( " await page.getByRole( ' button ' , { name: ' Menú global ' ,exact:true}).click();return true; " )
self . assertEqual ( self . code ( " return await page.getByRole( ' dialog ' ).locator( ' a[href= \" /settings/users \" ] ' ).count(); " ) , 0 )
self . capture ( " navigation-dashboard-menu-390 " )
self . code ( " await page.keyboard.press( ' Escape ' );return true; " )
def test_mobile_navigation_long_names_and_text_scaling ( self ) :
long_name = " Compañía sintética de soporte e infraestructura con identificacióntécnicaextraordinariamentelarga "
self . resize ( 320 , 640 )
self . code ( """ await page.route( ' **/api/companies { ,/**} ' ,async route=> {
const path = new URL ( route . request ( ) . url ( ) ) . pathname ;
if ( route . request ( ) . method ( ) != = ' GET ' | | ! [ " /api/companies " , " /api " + """ +json.dumps(self.company)+ """ ] . includes ( path ) ) { return route . fallback ( ) }
const response = await route . fetch ( ) ; const data = await response . json ( ) ;
if ( data . company ) { data . company . name = """ +json.dumps(long_name)+ """ }
if ( data . companies ) { data . companies = data . companies . map ( c = > ( { . . . c , name : """ +json.dumps(long_name)+ """ } ) ) }
await route . fulfill ( { response , json : data } ) ;
} ) ; return true ; """ )
try :
self . navigate ( ' assets ' )
self . code ( " await page.getByRole( ' button ' , { name: " + json . dumps ( ' Secciones de ' + long_name ) + " ,exact:true}).waitFor();return true; " )
metrics = self . code ( " return await page.evaluate(()=>( { mainY:document.querySelector( ' main ' ).getBoundingClientRect().y,overflow:document.documentElement.scrollWidth>innerWidth})); " )
self . record ( ' navigation-long-name-320 ' , metrics )
self . assertLessEqual ( metrics [ ' mainY ' ] , 128 )
self . assertFalse ( metrics [ ' overflow ' ] )
self . capture ( ' navigation-long-name-320 ' )
self . code ( " await page.getByRole( ' button ' , { name: " + json . dumps ( ' Secciones de ' + long_name ) + " ,exact:true}).click();return true; " )
self . resize ( 320 , 360 )
self . code ( " await page.getByRole( ' dialog ' ).locator( ' nav a ' ).last().focus();return true; " )
self . assertTrue ( self . code ( " return await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Cerrar navegación ' }).evaluate(e=> { const r=e.getBoundingClientRect();return r.top>=0&&r.bottom<=innerHeight;}); " ) )
self . assertTrue ( self . code ( " return await page.evaluate(()=>document.activeElement.getBoundingClientRect().bottom<=innerHeight); " ) )
self . capture ( ' navigation-short-window-320 ' )
self . code ( " await page.keyboard.press( ' Escape ' );await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });return true; " )
self . resize ( 390 , 844 )
zoom = self . code ( " const before=await page.evaluate(()=>devicePixelRatio);await page.keyboard.press( ' Control+Equal ' );await page.keyboard.press( ' Control+Equal ' );const after=await page.evaluate(()=>devicePixelRatio);await page.keyboard.press( ' Control+Digit0 ' );return { before,after,browserZoomSupported:after>before}; " )
self . record ( ' navigation-browser-zoom-attempt ' , zoom )
self . code ( " await page.evaluate(()=>document.documentElement.style.fontSize= ' 200 % ' );return true; " )
self . settle ( )
scaling = self . code ( " return await page.evaluate(()=>( { fontSize:getComputedStyle(document.documentElement).fontSize,mainY:document.querySelector( ' main ' ).getBoundingClientRect().y,overflow:document.documentElement.scrollWidth>innerWidth})); " )
self . record ( ' navigation-text-scale-200 ' , scaling )
self . assertFalse ( scaling [ ' overflow ' ] )
self . capture ( ' navigation-text-scale-200 ' )
finally :
self . code ( " await page.evaluate(()=>document.documentElement.style.fontSize= ' ' );await page.unroute( ' **/api/companies { ,/**} ' );return true; " )
def test_mobile_navigation_account_locale_and_edit ( self ) :
self . resize ( 390 , 844 )
self . navigate ( ' assets ' )
self . code ( " await page.getByRole( ' button ' , { name:/^Secciones de /}).click();await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Editar compañía ' ,exact:true}).click();await page.getByRole( ' dialog ' , { name: ' Editar compañía ' ,exact:true}).waitFor();return true; " )
self . capture ( ' navigation-company-edit-390 ' )
self . code ( " await page.getByRole( ' dialog ' , { name: ' Editar compañía ' ,exact:true}).getByRole( ' button ' , { name: ' Cancelar ' ,exact:true}).click();await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });await page.waitForFunction(()=>document.activeElement.getAttribute( ' aria-label ' )?.startsWith( ' Secciones de ' ));return true; " )
self . code ( " await page.locator( ' summary[aria-label= \" Menú de usuario \" ] ' ).click();await page.getByRole( ' combobox ' , { name: ' Idioma ' }).click();await page.getByRole( ' option ' , { name: ' English ' ,exact:true}).click();await page.getByRole( ' button ' , { name: ' Global menu ' ,exact:true}).waitFor();await page.locator( ' summary ' ).click();return true; " )
self . assertTrue ( self . code ( " return await page.locator( ' [data-slot=company-current-section] ' ).innerText()=== ' Assets ' ; " ) )
self . capture ( ' navigation-english-390 ' )
self . code ( " await page.getByRole( ' button ' , { name:/^Sections for /}).click();return true; " )
self . assertEqual ( self . code ( " return await page.getByRole( ' dialog ' ).locator( ' nav a ' ).count(); " ) , 21 )
self . assertEqual ( ' ' . join ( self . code ( " return await page.getByRole( ' dialog ' ).locator( ' nav [aria-current=page] ' ).innerText(); " ) . split ( ) ) , ' Assets1 ' )
self . code ( " await page.keyboard.press( ' Escape ' );await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });await page.getByRole( ' button ' , { name: ' Global menu ' ,exact:true}).click();return true; " )
for label in [ ' Home ' , ' Companies ' , ' Catalogs ' ] :
self . assertEqual ( self . code ( " return await page.getByRole( ' dialog ' ).getByRole( ' link ' , { name: " + json . dumps ( label ) + " ,exact:true}).count(); " ) , 1 )
self . code ( " await page.keyboard.press( ' Escape ' );await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });await page.locator( ' summary ' ).click();await page.getByRole( ' combobox ' , { name: ' Language ' }).click();await page.getByRole( ' option ' , { name: ' Español ' ,exact:true}).click();await page.locator( ' summary ' ).click();return true; " )
2026-10-21 17:31:09 +00:00
if __name__ == " __main__ " :
unittest . main ( )