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 + " } " } ) )
2026-10-21 21:35:56 +00:00
@classmethod
def evaluate ( cls , expression ) :
return cls . code ( " return await page.evaluate(()=> ( " + expression + " )); " )
2026-10-21 17:31:09 +00:00
@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 ) :
2026-10-21 21:35:56 +00:00
self . code ( " await page.evaluate(async()=> { await document.fonts.ready;await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))); await Promise.all(document.getAnimations().filter(a=>a.effect?.getTiming().iterations!==Infinity).map(a=>a.finished.catch(()=> {} )))}); return true; " )
2026-10-21 17:31:09 +00:00
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 ) :
2026-10-21 21:35:56 +00:00
sizes = [ ( 1920 , 1080 ) , ( 1661 , 900 ) , ( 1660 , 900 ) , ( 1659 , 900 ) , ( 1440 , 900 ) , ( 1366 , 768 ) , ( 1161 , 800 ) , ( 1160 , 800 ) , ( 1159 , 800 ) , ( 1100 , 800 ) , ( 1024 , 768 ) , ( 921 , 768 ) , ( 920 , 768 ) , ( 919 , 768 ) , ( 768 , 1024 ) , ( 390 , 844 ) , ( 320 , 640 ) ]
for width , height in sizes :
2026-10-21 20:55:13 +00:00
self . resize ( width , height )
self . navigate ( " assets " )
self . code ( " await page.getByRole( ' button ' , { name: ' Nuevo activo ' ,exact:true}).waitFor();return true; " )
2026-10-21 21:35:56 +00:00
self . settle ( )
metrics = self . code ( """ return await page.evaluate(()=> {
2026-10-21 20:55:13 +00:00
const r = e = > { const b = e . getBoundingClientRect ( ) ; return { x : b . x , y : b . y , width : b . width , height : b . height } } ;
2026-10-21 21:35:56 +00:00
const root = document . querySelector ( ' [data-slot=workspace] ' ) ; const visible = e = > e . getClientRects ( ) . length > 0 ;
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] ' ) ) , globalMode : root . dataset . globalNavigation , companyMode : root . dataset . companyNavigation , capacity : root . dataset . capacity , 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 ( visible ) . length , preference : localStorage . getItem ( ' metis-desk:sidebar-collapsed ' ) } ;
2026-10-21 20:55:13 +00:00
} ) ; """ )
self . record ( " navigation-geometry- " + str ( width ) , metrics )
self . capture ( " navigation-assets- " + str ( width ) )
2026-10-21 21:35:56 +00:00
self . assertFalse ( metrics [ ' overflow ' ] )
self . assertEqual ( metrics [ ' dialog ' ] , 0 )
self . assertEqual ( metrics [ ' globalMode ' ] , ' expanded ' if width > = 1660 else ' compact ' if width > = 920 else ' overlay ' )
self . assertEqual ( metrics [ ' companyMode ' ] , ' fixed ' if width > = 1160 else ' overlay ' )
self . assertEqual ( metrics [ ' visibleSections ' ] , 21 if width > = 1160 else 0 )
self . assertEqual ( metrics [ ' global ' ] [ ' width ' ] , 228 if width > = 1660 else 72 if width > = 920 else 0 )
self . assertEqual ( metrics [ ' main ' ] [ ' width ' ] , width - ( 228 if width > = 1660 else 72 if width > = 920 else 0 ) - ( 240 if width > = 1160 else 0 ) )
2026-10-21 20:55:13 +00:00
if width < 768 :
2026-10-21 21:35:56 +00:00
self . assertLessEqual ( metrics [ ' main ' ] [ ' y ' ] , 128 )
if width == 768 :
self . assertEqual ( metrics [ ' main ' ] [ ' width ' ] , 768 )
2026-10-21 20:55:13 +00:00
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 )
2026-10-21 21:35:56 +00:00
self . assertTrue ( self . code ( " return await page.getByRole( ' dialog ' ).isVisible(); " ) )
self . resize ( 920 , 768 )
2026-10-21 20:55:13 +00:00
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 ' )
2026-10-21 21:35:56 +00:00
for width in [ 768 , 1024 , 1366 ] :
self . resize ( width , 768 )
self . assertFalse ( self . evaluate ( " document.documentElement.scrollWidth>innerWidth " ) )
self . capture ( ' navigation-long-name- ' + str ( width ) )
if width > = 1160 :
self . code ( " await page.locator( ' [data-company-fixed-navigation] nav a ' ).last().focus();return true; " )
self . assertLessEqual ( self . evaluate ( " document.activeElement.getBoundingClientRect().bottom " ) , 768 )
self . resize ( 320 , 640 )
2026-10-21 20:55:13 +00:00
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 21:35:56 +00:00
def test_responsive_preferences_and_expansion ( self ) :
self . resize ( 1920 , 1080 )
self . navigate ( ' assets ' )
self . code ( " await page.evaluate(()=>localStorage.removeItem( ' metis-desk:sidebar-collapsed ' ));await page.reload();await page.getByRole( ' button ' , { name: ' Nuevo activo ' ,exact:true}).waitFor();return true; " )
try :
self . assertEqual ( self . evaluate ( " document.querySelector( ' [data-slot=workspace] ' ).dataset.globalNavigation " ) , ' expanded ' )
self . resize ( 1440 , 900 )
self . assertEqual ( self . evaluate ( " document.querySelector( ' [data-slot=workspace] ' ).dataset.globalNavigation " ) , ' compact ' )
self . assertIsNone ( self . evaluate ( " localStorage.getItem( ' metis-desk:sidebar-collapsed ' ) " ) )
self . code ( " await page.getByRole( ' button ' , { name: ' Expandir menú ' ,exact:true}).click();await page.getByRole( ' dialog ' ).waitFor();return true; " )
self . capture ( ' responsive-expanded-overlay-1440 ' )
self . assertIsNone ( self . evaluate ( " localStorage.getItem( ' metis-desk:sidebar-collapsed ' ) " ) )
self . code ( " await page.keyboard.press( ' Escape ' );await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });await page.waitForFunction(()=>document.activeElement.getAttribute( ' aria-label ' )=== ' Expandir menú ' );return true; " )
self . resize ( 1920 , 1080 )
self . code ( " await page.getByRole( ' button ' , { name: ' Contraer menú ' ,exact:true}).click();return true; " )
self . assertEqual ( self . evaluate ( " localStorage.getItem( ' metis-desk:sidebar-collapsed ' ) " ) , ' true ' )
self . resize ( 768 , 1024 )
self . resize ( 1920 , 1080 )
self . assertEqual ( self . evaluate ( " document.querySelector( ' [data-slot=workspace] ' ).dataset.globalNavigation " ) , ' compact ' )
self . code ( " await page.reload();await page.getByRole( ' button ' , { name: ' Expandir menú ' ,exact:true}).waitFor();return true; " )
self . assertEqual ( self . evaluate ( " document.querySelector( ' [data-slot=workspace] ' ).dataset.globalNavigation " ) , ' compact ' )
self . code ( " await page.getByRole( ' button ' , { name: ' Expandir menú ' ,exact:true}).click();return true; " )
self . assertEqual ( self . evaluate ( " localStorage.getItem( ' metis-desk:sidebar-collapsed ' ) " ) , ' false ' )
self . record ( ' responsive-preference-restored ' , self . evaluate ( " { mode:document.querySelector( ' [data-slot=workspace] ' ).dataset.globalNavigation,stored:localStorage.getItem( ' metis-desk:sidebar-collapsed ' )} " ) )
finally :
self . code ( " await page.evaluate(()=>localStorage.removeItem( ' metis-desk:sidebar-collapsed ' ));await page.reload();return true; " )
def test_responsive_context_intention_and_content ( self ) :
self . resize ( 1366 , 768 )
self . navigate ( ' assets ' )
self . code ( " await page.getByRole( ' textbox ' , { name: ' Buscar ' ,exact:true}).fill( ' AUDIT ' );await page.getByRole( ' link ' , { name: ' AUDIT-PORT-01 ' ,exact:true}).waitFor();await page.evaluate(()=>window.auditTable=document.querySelector( ' main table ' ));await page.getByRole( ' button ' , { name: ' Ocultar secciones ' ,exact:true}).click();await page.getByRole( ' button ' , { name:/^Secciones de /}).waitFor();return true; " )
self . assertTrue ( self . evaluate ( " document.querySelector( ' main table ' )===window.auditTable " ) )
self . assertEqual ( self . code ( " return await page.getByRole( ' textbox ' , { name: ' Buscar ' ,exact:true}).inputValue(); " ) , ' AUDIT ' )
self . capture ( ' responsive-context-hidden-1366 ' )
self . resize ( 1024 , 768 )
self . resize ( 1366 , 768 )
self . assertEqual ( self . evaluate ( " document.querySelector( ' [data-slot=workspace] ' ).dataset.companyNavigation " ) , ' overlay ' )
self . code ( " await page.getByRole( ' button ' , { name:/^Secciones de /}).click();await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Fijar secciones ' ,exact:true}).click();await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });await page.waitForFunction(()=>!!document.activeElement.closest( ' [data-company-fixed-navigation] ' ));return true; " )
self . assertTrue ( self . evaluate ( " document.querySelector( ' main table ' )===window.auditTable " ) )
self . assertEqual ( self . evaluate ( " document.querySelector( ' [data-slot=workspace] ' ).dataset.companyNavigation " ) , ' fixed ' )
self . code ( " await page.getByRole( ' textbox ' , { name: ' Buscar ' ,exact:true}).fill( ' ' );return true; " )
self . resize ( 1024 , 768 )
self . code ( " await page.getByRole( ' button ' , { name:/^Secciones de /}).click();return true; " )
self . assertTrue ( self . code ( " return await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Fijar secciones ' ,exact:true}).isDisabled(); " ) )
self . capture ( ' responsive-context-overlay-1024 ' )
self . code ( " await page.keyboard.press( ' Escape ' );return true; " )
def test_responsive_resize_focus_and_modality ( self ) :
self . resize ( 1024 , 768 )
self . navigate ( ' assets ' )
self . code ( " await page.getByRole( ' button ' , { name:/^Secciones de /}).click();await page.getByRole( ' dialog ' ).getByRole( ' link ' , { name:/^Personas/}).focus();return true; " )
self . resize ( 1366 , 768 )
self . code ( " await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });await page.waitForFunction(()=>document.activeElement.getAttribute( ' href ' )?.endsWith( ' /people ' ));return true; " )
self . assertTrue ( self . evaluate ( " !!document.activeElement.closest( ' [data-company-fixed-navigation] ' ) " ) )
self . resize ( 1024 , 768 )
self . code ( " await page.waitForFunction(()=>document.activeElement.dataset.workspaceNavigationTrigger=== ' company ' );return true; " )
self . assertEqual ( self . code ( " return await page.getByRole( ' dialog ' ).count(); " ) , 0 )
self . code ( " await page.getByRole( ' link ' , { name: ' AUDIT-PORT-01 ' ,exact:true}).focus();return true; " )
self . resize ( 768 , 1024 )
self . assertEqual ( self . evaluate ( " document.activeElement.textContent " ) , ' AUDIT-PORT-01 ' )
self . code ( " await page.getByRole( ' button ' , { name: ' Menú global ' ,exact:true}).click();await page.getByRole( ' dialog ' ).getByRole( ' link ' , { name: ' Compañías ' ,exact:true}).focus();return true; " )
self . resize ( 1024 , 768 )
self . code ( " await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });await page.waitForFunction(()=>document.activeElement.getAttribute( ' href ' )=== ' /companies ' );return true; " )
self . resize ( 768 , 1024 )
self . code ( " await page.waitForFunction(()=>document.activeElement.dataset.workspaceNavigationTrigger=== ' global ' );return true; " )
self . assertEqual ( self . code ( " return await page.getByRole( ' dialog ' ).count(); " ) , 0 )
self . resize ( 1920 , 1080 )
self . code ( " await page.locator( ' [data-workspace-desktop-navigation] a[href= \" /companies \" ] ' ).focus();return true; " )
self . resize ( 1440 , 900 )
self . assertEqual ( self . evaluate ( " document.activeElement.getAttribute( ' href ' ) " ) , ' /companies ' )
self . record ( ' responsive-resize-final-focus ' , self . evaluate ( " { tag:document.activeElement.tagName,href:document.activeElement.getAttribute( ' href ' ),visible:document.activeElement.getClientRects().length>0,dialogs:document.querySelectorAll( ' [role=dialog] ' ).length,bodyOverflow:getComputedStyle(document.body).overflow} " ) )
self . capture ( ' responsive-resize-final-1440 ' )
def test_responsive_scroll_tooltips_and_form_stability ( self ) :
self . resize ( 1366 , 768 )
self . navigate ( ' assets ' )
self . code ( " await page.keyboard.press( ' Tab ' );await page.locator( ' [data-workspace-desktop-navigation] a[href= \" /companies \" ] ' ).focus();await page.getByRole( ' tooltip ' ).filter( { hasText: ' Compañías ' }).waitFor();return true; " )
self . capture ( ' responsive-tooltip-focus-1366 ' )
self . code ( " await page.getByRole( ' link ' , { name: ' AUDIT-PORT-01 ' ,exact:true}).focus();await page.mouse.move(500,600);await page.locator( ' [data-workspace-desktop-navigation] a[href= \" /companies \" ] ' ).hover();await page.getByRole( ' tooltip ' ).filter( { hasText: ' Compañías ' }).waitFor();return true; " )
self . capture ( ' responsive-tooltip-hover-1366 ' )
self . code ( " await page.mouse.move(500,600);await page.locator( ' [data-company-fixed-navigation] nav a ' ).last().focus();return true; " )
scroll = self . evaluate ( " { nav:document.querySelector( ' [data-company-navigation-scroll] ' ).scrollTop,main:document.querySelector( ' [data-slot=workspace-scroll] ' ).scrollTop,bottom:document.activeElement.getBoundingClientRect().bottom} " )
self . record ( ' responsive-independent-scroll ' , scroll )
self . assertGreater ( scroll [ ' nav ' ] , 0 )
self . assertEqual ( scroll [ ' main ' ] , 0 )
self . assertLessEqual ( scroll [ ' bottom ' ] , 768 )
self . capture ( ' responsive-last-destination-1366 ' )
self . navigate ( ' assets ' )
self . code ( " await page.getByRole( ' button ' , { name: ' Nuevo activo ' ,exact:true}).click();await page.getByRole( ' dialog ' ).waitFor();await page.evaluate(()=>window.auditForm=document.querySelector( ' [role=dialog] ' ));return true; " )
self . resize ( 768 , 1024 )
self . assertTrue ( self . evaluate ( " document.querySelector( ' [role=dialog] ' )===window.auditForm&&!!document.activeElement.closest( ' [role=dialog] ' ) " ) )
self . code ( " await page.keyboard.press( ' Escape ' );await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });return true; " )
self . resize ( 1024 , 768 )
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=700);return true; " )
self . settle ( )
self . resize ( 1366 , 768 )
self . code ( " await page.locator( ' main button ' ).first().focus();return true; " )
self . record ( ' responsive-sticky-1366 ' , self . evaluate ( " { header:document.querySelector( ' header ' ).getBoundingClientRect().top,nav:document.querySelector( ' [data-company-fixed-navigation] ' ).getBoundingClientRect().top,focused:document.activeElement.getBoundingClientRect().top,overflow:document.documentElement.scrollWidth>innerWidth} " ) )
self . assertGreaterEqual ( self . evaluate ( " document.activeElement.getBoundingClientRect().top " ) , 88 )
self . capture ( ' responsive-detail-scrolled-1366 ' )
def test_responsive_container_capacity ( self ) :
self . resize ( 1920 , 1080 )
self . navigate ( ' assets ' )
self . code ( " await page.getByRole( ' button ' , { name: ' Nuevo activo ' ,exact:true}).waitFor();await page.evaluate(()=> { window.auditContainerTable=document.querySelector( ' main table ' );document.querySelector( ' [data-slot=workspace] ' ).style.width= ' 1024px ' });await page.waitForFunction(()=>document.querySelector( ' [data-slot=workspace] ' ).dataset.capacity=== ' rail ' );return true; " )
try :
metrics = self . evaluate ( " ( { viewport:innerWidth,shell:document.querySelector( ' [data-slot=workspace] ' ).getBoundingClientRect().width,global:document.querySelector( ' [data-slot=workspace] ' ).dataset.globalNavigation,context:document.querySelector( ' [data-slot=workspace] ' ).dataset.companyNavigation,sameTable:document.querySelector( ' main table ' )===window.auditContainerTable}) " )
self . record ( ' responsive-contained-shell ' , metrics )
self . assertEqual ( metrics , { ' viewport ' : 1920 , ' shell ' : 1024 , ' global ' : ' compact ' , ' context ' : ' overlay ' , ' sameTable ' : True } )
self . capture ( ' responsive-contained-shell-1024 ' )
finally :
self . code ( " await page.evaluate(()=>document.querySelector( ' [data-slot=workspace] ' ).style.width= ' ' );await page.waitForFunction(()=>document.querySelector( ' [data-slot=workspace] ' ).dataset.capacity=== ' wide ' );return true; " )
def test_responsive_header_focus ( self ) :
for start , end , panel , label in [ ( 390 , 768 , ' global ' , ' Menú global ' ) , ( 390 , 768 , ' search ' , ' Buscar en Metis Desk ' ) , ( 1024 , 1366 , ' company ' , ' Secciones de ' ) , ( 768 , 1024 , ' global ' , ' Menú global ' ) ] :
self . resize ( start , 844 )
self . navigate ( ' assets ' )
if panel == ' company ' :
self . code ( " await page.getByRole( ' button ' , { name:/^Secciones de /}).focus();return true; " )
else :
self . code ( " await page.getByRole( ' button ' , { name: " + json . dumps ( label ) + " ,exact:true}).focus();return true; " )
self . resize ( end , 900 )
focus = self . evaluate ( " ( { tag:document.activeElement.tagName,label:document.activeElement.getAttribute( ' aria-label ' ),visible:document.activeElement.getClientRects().length>0,body:document.activeElement===document.body}) " )
self . record ( ' responsive-header-focus- ' + panel + ' - ' + str ( start ) + ' - ' + str ( end ) , focus )
self . assertFalse ( focus [ ' body ' ] , focus )
self . assertTrue ( focus [ ' visible ' ] , focus )
def test_responsive_company_switching ( self ) :
# Solo crea una compañía sintética en la BD desechable; no cambia usuarios ni datos reales.
name = ' Empresa sintética secundaria V01.2 '
self . resize ( 1024 , 768 )
self . tool ( ' browser_navigate ' , { ' url ' : self . origin + ' /companies ' } )
self . code ( " await page.getByRole( ' button ' , { name: ' Nueva compañía ' ,exact:true}).waitFor();return true; " )
present = self . code ( " return await page.locator( ' main ' ).getByRole( ' link ' , { name: " + json . dumps ( name ) + " ,exact:true}).count(); " )
if not present :
self . code ( " await page.route( ' **/api/companies ' ,r=>r.request().method()=== ' POST ' ?r.continue():r.fallback());await page.getByRole( ' button ' , { name: ' Nueva compañía ' ,exact:true}).click();await page.getByRole( ' dialog ' ).getByRole( ' textbox ' , { name:/^Nombre/}).fill( " + json . dumps ( name ) + " );await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Guardar ' ,exact:true}).click();await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });await page.unroute( ' **/api/companies ' );return true; " )
self . resize ( 768 , 1024 )
self . code ( " await page.getByRole( ' button ' , { name: ' Menú global ' ,exact:true}).click();await page.getByRole( ' dialog ' ).getByRole( ' link ' , { name: " + json . dumps ( name ) + " ,exact:true}).click();await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });await page.getByRole( ' button ' , { name: " + json . dumps ( ' Secciones de ' + name ) + " ,exact:true}).waitFor();return true; " )
second = self . code ( " return new URL(page.url()).pathname; " )
self . assertNotEqual ( second , self . company )
self . capture ( ' responsive-second-company-768 ' )
self . code ( " await page.getByRole( ' button ' , { name: " + json . dumps ( ' Secciones de ' + name ) + " ,exact:true}).click();await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Cambiar compañía ' }).click();await page.getByRole( ' dialog ' ).getByRole( ' link ' , { name: ' Auditoría visual — Datos sintéticos ' ,exact:true}).click();await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });return true; " )
self . assertEqual ( self . code ( " return new URL(page.url()).pathname; " ) , self . company )
self . record ( ' responsive-company-switch ' , { ' second ' : second , ' returnedTo ' : self . company } )
2026-10-21 17:31:09 +00:00
if __name__ == " __main__ " :
unittest . main ( )