2026-10-25 10:58:26 +00:00
#!/usr/bin/env python3
""" Recorrido MCP del mapa en el entorno desechable documentado; no modifica datos reales. """
import json
import os
from pathlib import Path
import runpy
import unittest
ROOT = Path ( __file__ ) . resolve ( ) . parent . parent
BASE = runpy . run_path ( str ( ROOT / ' scripts/workstation-dashboard-ui.test.py ' ) )
class WorkplaceVisualMapTests ( BASE [ ' WorkstationDashboardTests ' ] ) :
@classmethod
def setUpClass ( cls ) :
super ( ) . setUpClass ( )
phase = os . environ . get ( ' METIS_MAP_CAPTURE_PHASE ' , ' after ' )
if phase not in ( ' before ' , ' after ' ) :
raise RuntimeError ( ' Fase de captura no válida. ' )
cls . phase = phase
cls . output = cls . output . parent / ' visual-map-iteration-2 ' / phase
cls . output . mkdir ( parents = True , exist_ok = True )
evidence = cls . output / " measurements.json "
cls . evidence = json . loads ( evidence . read_text ( ) ) if evidence . exists ( ) else [ ]
cls . code ( " page.__mapNetwork= { active:0,waiters:[]};await page.route( ' **/api/** ' ,async route=> { const gate=page.__mapNetwork;if(gate.active<2)gate.active++;else await new Promise(resolve=>gate.waiters.push(resolve));try { const response=await route.fetch();await route.fulfill( {response} );}finally { const next=gate.waiters.shift();if(next)next();else gate.active--}});return true; " )
def map_ready ( self , key = ' main ' , software = True , pending = False ) :
self . navigate ( key , pending = key != ' empty ' and pending )
self . code ( " await page.getByRole( ' heading ' , { name: ' Mapa visual del puesto ' ,exact:true}).waitFor();return true; " )
if key == ' main ' and software :
self . code ( " await page.getByRole( ' link ' , { name: ' Gestor documental de ejemplo ' ,exact:true}).first().waitFor();return true; " )
self . settle ( )
def test_map ( 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 . map_ready ( pending = True )
self . code ( " await page.getByRole( ' heading ' , { name: ' Mapa visual del puesto ' ,exact:true}).scrollIntoViewIfNeeded();return true; " )
geometry = self . code ( """ return await page.evaluate(()=> {
const map = document . querySelector ( ' [data-workplace-map] ' ) ; const box = map . getBoundingClientRect ( ) ;
const nodes = [ . . . map . querySelectorAll ( ' article ' ) ] . map ( e = > { const b = e . getBoundingClientRect ( ) ; return { text : e . innerText , kind : e . dataset . workplaceEquipment ? ? ' software ' , x : b . x , y : b . y , width : b . width , height : b . height , scrollWidth : e . scrollWidth , clientWidth : e . clientWidth , scrollHeight : e . scrollHeight , clientHeight : e . clientHeight } } ) ;
const canvas = map . querySelector ( ' [data-workplace-canvas] ' ) ? . getBoundingClientRect ( ) ; const heading = map . querySelector ( ' .react-flow__node-softwareHeading ' ) ; const h = heading ? . getBoundingClientRect ( ) ; return { viewport : innerWidth , scrollWidth : document . documentElement . scrollWidth , mapWidth : box . width , canvas : ! ! canvas , canvasBounds : canvas ? { left : canvas . left , right : canvas . right , top : canvas . top , bottom : canvas . bottom , height : canvas . height } : null , heading : h ? { x : h . x , y : h . y , width : h . width , height : h . height , text : heading . innerText , tabIndex : heading . getAttribute ( ' tabindex ' ) } : null , handlesHidden : [ . . . map . querySelectorAll ( ' .react-flow__handle ' ) ] . every ( e = > getComputedStyle ( e ) . opacity == = ' 0 ' ) , nodes , edges : map . querySelectorAll ( ' .react-flow__edge ' ) . length } ;
} ) ; """ )
self . record ( f ' layout- { width } ' , geometry )
self . assertLessEqual ( geometry [ ' scrollWidth ' ] , width )
self . assertGreaterEqual ( len ( geometry [ ' nodes ' ] ) , 3 )
self . assertEqual ( geometry [ ' canvas ' ] , geometry [ ' mapWidth ' ] > = 640 )
if geometry [ ' canvas ' ] :
self . assertEqual ( geometry [ ' edges ' ] , len ( geometry [ ' nodes ' ] ) - 1 )
for node in geometry [ ' nodes ' ] :
self . assertGreater ( node [ ' width ' ] , 140 )
self . assertLessEqual ( node [ ' scrollWidth ' ] , node [ ' clientWidth ' ] + 1 )
if self . phase == ' after ' :
self . assertLessEqual ( node [ ' scrollHeight ' ] , node [ ' clientHeight ' ] + 1 )
if geometry [ ' canvas ' ] :
for index , first in enumerate ( geometry [ ' nodes ' ] ) :
for second in geometry [ ' nodes ' ] [ index + 1 : ] :
self . assertTrue ( first [ ' x ' ] + first [ ' width ' ] < = second [ ' x ' ] + 1 or second [ ' x ' ] + second [ ' width ' ] < = first [ ' x ' ] + 1 or first [ ' y ' ] + first [ ' height ' ] < = second [ ' y ' ] + 1 or second [ ' y ' ] + second [ ' height ' ] < = first [ ' y ' ] + 1 , ' Los nodos no se solapan ' )
if self . phase == ' after ' and geometry [ ' canvas ' ] :
main = next ( node for node in geometry [ ' nodes ' ] if node [ ' kind ' ] == ' principal ' )
for node in geometry [ ' nodes ' ] :
if node [ ' kind ' ] == ' associated ' :
self . assertGreater ( main [ ' width ' ] , node [ ' width ' ] )
self . assertGreater ( main [ ' height ' ] , node [ ' height ' ] )
bounds = geometry [ ' canvasBounds ' ]
self . assertGreaterEqual ( node [ ' x ' ] , bounds [ ' left ' ] - 1 )
self . assertLessEqual ( node [ ' x ' ] + node [ ' width ' ] , bounds [ ' right ' ] + 1 )
self . assertGreaterEqual ( node [ ' y ' ] , bounds [ ' top ' ] - 1 )
self . assertLessEqual ( node [ ' y ' ] + node [ ' height ' ] , bounds [ ' bottom ' ] + 1 )
self . assertEqual ( geometry [ ' heading ' ] [ ' text ' ] , ' Software del ordenador principal ' )
self . assertIsNone ( geometry [ ' heading ' ] [ ' tabIndex ' ] )
self . assertTrue ( geometry [ ' handlesHidden ' ] )
self . assertLessEqual ( bounds [ ' height ' ] , 500 )
self . capture ( f ' map- { width } ' )
def test_dense ( self ) :
self . tool ( ' browser_resize ' , { ' width ' : 1600 , ' height ' : 1000 } )
self . map_ready ( ' dense ' , software = False )
self . assertEqual ( self . code ( " return await page.locator( ' [data-workplace-map] article ' ).count(); " ) , 3 )
self . code ( " await page.locator( ' [data-workplace-coverage=equipment] ' ).getByText( ' Mostrando 3 de 80 activos ' , { exact:true}).waitFor();return true; " )
self . capture ( ' dense ' )
self . code ( " await page.getByRole( ' button ' , { name: ' Ver todo el equipamiento ' ,exact:true}).click();await page.getByRole( ' region ' , { name:/^Equipamiento/}).getByRole( ' button ' , { name: ' Mostrar todos ' ,exact:true}).click();return true; " )
count = self . code ( " return await page.getByRole( ' region ' , { name:/^Equipamiento/}).locator( ' [data-slot=compact-list-item] ' ).count(); " )
self . assertEqual ( count , 80 )
self . record ( ' dense ' , { ' members ' : count , ' profiles ' : 60 , ' visibleMapNodes ' : 3 } )
def test_navigation_and_management ( self ) :
self . tool ( ' browser_resize ' , { ' width ' : 1440 , ' height ' : 900 } )
self . map_ready ( )
target = self . base + ' /assets/ ' + self . fixture [ ' host ' ]
keyboard = self . code ( " await page.getByRole( ' button ' , { name: ' Gestionar equipamiento ' ,exact:true}).focus();await page.keyboard.press( ' Tab ' );return await page.evaluate(()=>( { name:document.activeElement?.getAttribute( ' aria-label ' ),outline:getComputedStyle(document.activeElement).outlineStyle})); " )
self . assertIn ( ' PORT-ADM-01 ' , keyboard [ ' name ' ] )
self . assertEqual ( keyboard [ ' outline ' ] , ' solid ' )
self . record ( ' keyboard ' , keyboard )
result = self . code ( " const link=page.locator( ' [data-workplace-map] ' ).getByRole( ' link ' , { name:/PORT-ADM-01/});await link.focus();const focused=await link.evaluate(e=>e===document.activeElement);await page.keyboard.press( ' Enter ' );await page.locator( ' main h1 ' ).waitFor();return { focused,url:new URL(page.url()).pathname}; " )
self . assertTrue ( result [ ' focused ' ] )
self . assertEqual ( result [ ' url ' ] , target )
self . map_ready ( )
self . code ( " await page.getByRole( ' button ' , { name: ' Gestionar equipamiento ' ,exact:true}).click();await page.getByRole( ' dialog ' , { name: ' Editar puesto ' ,exact:true}).waitFor();await page.getByRole( ' dialog ' ).getByRole( ' button ' , { name: ' Cancelar ' ,exact:true}).click();await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });return true; " )
self . assertTrue ( self . code ( " return await page.getByRole( ' button ' , { name: ' Gestionar equipamiento ' ,exact:true}).evaluate(e=>e===document.activeElement); " ) )
remote = self . code ( " const trigger=page.getByRole( ' button ' , { name: ' Contexto remoto ' ,exact:true});await trigger.focus();await page.keyboard.press( ' Space ' );const expanded=await trigger.getAttribute( ' aria-expanded ' );await page.keyboard.press( ' Space ' );return { expanded,collapsed:await trigger.getAttribute( ' aria-expanded ' )}; " )
self . assertEqual ( remote , { ' expanded ' : ' true ' , ' collapsed ' : ' false ' } )
self . expand_primary ( )
self . code ( " await page.getByRole( ' heading ' , { name:/Personas y perfiles/}).scrollIntoViewIfNeeded();return true; " )
self . capture ( ' profiles-1440 ' )
self . code ( " await page.getByRole( ' button ' , { name: ' Gestionar software ' ,exact:true}).click();await page.getByRole( ' dialog ' ).waitFor();await page.keyboard.press( ' Escape ' );await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });await page.getByRole( ' tab ' , { name: ' Historial ' ,exact:true}).click();await page.getByRole( ' heading ' , { name: ' Historial del puesto ' ,exact:true}).waitFor();return true; " )
self . record ( ' navigation-management ' , result )
def test_partial_errors_and_empty ( self ) :
self . tool ( ' browser_resize ' , { ' width ' : 1280 , ' height ' : 900 } )
self . addCleanup ( self . code , " await page.unroute( ' **/api/software-installations/** ' );await page.unroute( ' **/api/mail-configurations/** ' );return true; " )
self . map_ready ( ' empty ' )
self . assertEqual ( self . code ( " return await page.locator( ' [data-workplace-map] article ' ).count(); " ) , 0 )
self . capture ( ' without-principal ' )
self . map_ready ( ' main ' )
self . code ( " await page.route( ' **/api/software-installations/** ' ,route=>route.fulfill( { status:500,json: { code: ' INTERNAL_ERROR ' }}));return true; " )
self . map_ready ( ' main ' , software = False )
self . assertGreaterEqual ( self . code ( " return await page.locator( ' [data-workplace-map] article ' ).count(); " ) , 2 )
self . code ( " await page.getByRole( ' region ' , { name: ' Mapa visual del puesto ' ,exact:true}).getByRole( ' alert ' ).first().waitFor();return true; " )
self . capture ( ' software-error ' )
self . record ( ' partial-error ' , { ' equipmentRemains ' : True } )
self . code ( " await page.unroute( ' **/api/software-installations/** ' );await page.route( ' **/api/software-installations/** ' ,route=>route.fulfill( { status:200,json: { configurations:[]}}));return true; " )
self . map_ready ( ' main ' , software = False )
self . code ( " await page.getByRole( ' region ' , { name: ' Mapa visual del puesto ' ,exact:true}).getByText( ' No hay software documentado para el ordenador principal. ' , { exact:true}).waitFor();return true; " )
self . capture ( ' without-software ' )
self . code ( " await page.route( ' **/api/mail-configurations/** ' ,route=>route.fulfill( { status:500,json: { code: ' INTERNAL_ERROR ' }}));return true; " )
self . map_ready ( ' main ' , software = False , pending = False )
self . code ( " await page.getByRole( ' region ' , { name: ' Configuraciones pendientes ' ,exact:true}).getByRole( ' alert ' ).first().waitFor();return true; " )
self . assertGreaterEqual ( self . code ( " return await page.locator( ' [data-workplace-map] article ' ).count(); " ) , 2 )
self . capture ( ' configuration-error ' )
self . record ( ' partial-configurations ' , { ' equipmentRemains ' : True } )
def test_principal_only_and_resize ( self ) :
self . tool ( ' browser_resize ' , { ' width ' : 1600 , ' height ' : 1000 } )
self . map_ready ( ' withoutResponsible ' , software = False )
self . code ( " await page.getByText( ' No hay software documentado para el ordenador principal. ' , { exact:true}).waitFor();return true; " )
initial = self . code ( " return await page.locator( ' [data-workplace-canvas] ' ).evaluate(e=>( { height:e.getBoundingClientRect().height,nodes:e.querySelectorAll( ' article ' ).length})); " )
self . assertEqual ( initial [ ' nodes ' ] , 1 )
self . assertLess ( initial [ ' height ' ] , 300 )
self . capture ( ' principal-only ' )
self . record ( ' principal-only ' , initial )
for width in [ 1280 , 768 , 390 , 1440 ] :
self . tool ( ' browser_resize ' , { ' width ' : width , ' height ' : 1000 } )
self . code ( " await page.waitForFunction(()=> { const map=document.querySelector( ' [data-workplace-map] ' );return !!map.querySelector( ' .react-flow ' )===(map.getBoundingClientRect().width>=640)});return true; " )
self . settle ( )
geometry = self . code ( " return await page.evaluate(()=>( { width:innerWidth,scrollWidth:document.documentElement.scrollWidth,nodes:document.querySelectorAll( ' [data-workplace-map] article ' ).length,name:document.querySelector( ' [data-workplace-map] a ' ).getAttribute( ' aria-label ' )})); " )
self . assertEqual ( geometry [ ' nodes ' ] , 1 )
self . assertLessEqual ( geometry [ ' scrollWidth ' ] , width )
self . assertIn ( ' PC-LAB-01 ' , geometry [ ' name ' ] )
self . record ( f ' resize- { width } ' , geometry )
def test_software_coverage_states_and_refetch ( self ) :
self . tool ( ' browser_resize ' , { ' width ' : 1600 , ' height ' : 1000 } )
self . map_ready ( )
2026-10-25 12:46:06 +00:00
self . addCleanup ( self . code , " await page.unroute( ' **/api/software-installations/** ' );await page.unroute( ' **/api/resources/** ' );await page.unroute( ' **/api/resource-application-references/**/resolve ' );return true; " )
2026-10-25 10:58:26 +00:00
prepared = self . code ( """ const res=await page.request.get( ' """ + self . origin + """ /api/resources/ """ + self . fixture [ ' companyId ' ] + """ ' );if(res.status()!==200)throw Error( ' Recursos sintéticos no disponibles ' );const inventory=await res.json();page.__mapApplications=inventory.resources.filter(r=>r.values.name.startsWith( ' AUDIT-VOLUME-APP- ' )).slice(0,8);if(page.__mapApplications.length!==8)throw Error( ' Se requieren ocho aplicaciones sintéticas ' );
await page . route ( ' **/api/resources/** ' , async route = > { const response = await route . fetch ( ) ; const data = await response . json ( ) ; for ( const resource of data . resources ? ? [ ] ) { if ( page . __mapApplications . some ( r = > r . id == = resource . id ) ) resource . values . name = ' Suite de documentación y administración — ' + resource . values . name + ' — nombre largo para teclado ' ; } await route . fulfill ( { response , json : data } ) ; } ) ;
2026-10-25 12:46:06 +00:00
await page . route ( ' **/api/resource-application-references/**/resolve ' , async route = > { const response = await route . fetch ( ) ; const data = await response . json ( ) ; for ( const item of data . items ? ? [ ] ) { if ( item . status == = ' resolved ' & & page . __mapApplications . some ( r = > r . id == = item . id ) ) item . reference . name = ' Suite de documentación y administración — ' + item . reference . name + ' — nombre largo para teclado ' ; } await route . fulfill ( { response , json : data } ) ; } ) ;
2026-10-25 10:58:26 +00:00
page . __mapSoftwareFailure = false ;
await page . route ( ' **/api/software-installations/** ' , async route = > { if ( page . __mapSoftwareFailure ) { await route . fulfill ( { status : 500 , json : { code : ' INTERNAL_ERROR ' } } ) ; return ; } const response = await route . fetch ( ) ; const data = await response . json ( ) ; const base = data . configurations . find ( c = > c . values . assetId == = ' " " " +self.fixture[ ' host ' ]+ " " " ' ) ; if ( ! base ) throw Error ( ' Falta instalación base ' ) ; const configurations = page . __mapApplications . map ( ( resource , index ) = > { const state = [ ' configured ' , ' pending ' , ' removal_pending ' ] [ index % 3 ] ; const values = { . . . base . values , resourceId : resource . id , licenseResourceId : null , state } ; return { . . . base , id : ' 0199b200-0000-7000-8000- ' + String ( 800 + index ) . padStart ( 12 , ' 0 ' ) , values , needsReview : state == = ' removal_pending ' | | ! resource . values . active , history : base . history . map ( revision = > ( { . . . revision , values } ) ) } ; } ) ; await route . fulfill ( { response , json : { . . . data , configurations : [ . . . data . configurations . filter ( c = > c . values . assetId != = base . values . assetId ) , . . . configurations ] } } ) ; } ) ; return { installations : page . __mapApplications . length } ; """ )
self . record ( ' software-scenario ' , prepared )
self . map_ready ( software = False )
self . code ( " await page.getByText( ' Mostrando 3 de 8 instalaciones de software ' , { exact:true}).waitFor();return true; " )
states = self . code ( " return await page.locator( ' [data-workplace-map] [data-workplace-software] [data-slot=badge] ' ).allTextContents(); " )
labels = json . loads ( ( ROOT / ' modules/companies/src/common/locales/es.json ' ) . read_text ( ) ) [ ' companies ' ] [ ' mail ' ]
self . assertCountEqual ( states , [ labels [ state ] for state in [ ' configured ' , ' pending ' , ' removal_pending ' ] ] )
self . assertTrue ( self . code ( " return await page.evaluate(()=>[...document.querySelectorAll( ' [data-workplace-software] ' )].some(e=>e.textContent.includes( ' Requiere revisión ' ))); " ) )
self . code ( " const link=page.locator( ' [data-workplace-map] [data-workplace-software] a ' ).first();page.__mapTooltipName=await link.getAttribute( ' aria-label ' );await link.focus();await page.keyboard.press( ' Tab ' );await page.keyboard.press( ' Shift+Tab ' );await page.locator( ' [data-slot=tooltip-content] ' ).filter( { hasText:page.__mapTooltipName}).waitFor();return true; " )
self . assertTrue ( self . code ( " return (await page.locator( ' [data-slot=tooltip-content] ' ).filter( { hasText:page.__mapTooltipName}).innerText()).includes( ' nombre largo para teclado ' ); " ) )
self . code ( " await page.keyboard.press( ' Escape ' );return true; " )
sizes = self . code ( " return await page.locator( ' [data-workplace-map] [data-workplace-software] ' ).evaluateAll(nodes=>nodes.map(e=>( { height:e.clientHeight,scrollHeight:e.scrollHeight,width:e.clientWidth,scrollWidth:e.scrollWidth}))); " )
self . assertTrue ( all ( size [ ' scrollHeight ' ] < = size [ ' height ' ] + 1 and size [ ' scrollWidth ' ] < = size [ ' width ' ] + 1 for size in sizes ) )
self . record ( ' long-titles ' , sizes )
self . capture ( ' software-coverage-states ' )
self . code ( " await page.getByRole( ' button ' , { name: ' Ver todo el software ' ,exact:true}).click();return true; " )
self . assertTrue ( self . code ( " return await page.getByRole( ' button ' , { name: ' Gestionar software ' ,exact:true}).evaluate(e=>e===document.activeElement); " ) )
self . code ( " await page.getByRole( ' button ' , { name: ' Gestionar software ' ,exact:true}).click();await page.getByRole( ' dialog ' ).waitFor();await page.keyboard.press( ' Escape ' );await page.getByRole( ' dialog ' ).waitFor( { state: ' hidden ' });await page.getByRole( ' heading ' , { name: ' Mapa visual del puesto ' ,exact:true}).scrollIntoViewIfNeeded();return true; " )
self . code ( " await page.evaluate(()=> { window.__mapRetained=document.querySelector( ' [data-workplace-canvas] ' );window.__mapCamera=document.querySelector( ' [data-workplace-canvas] .react-flow__viewport ' ).style.transform;});return true; " )
self . code ( " const response=page.waitForResponse(r=>r.url().includes( ' /api/software-installations/ ' ));await page.evaluate(()=> { Object.defineProperty(document, ' visibilityState ' , { configurable:true,value: ' hidden ' });window.dispatchEvent(new Event( ' visibilitychange ' ));Object.defineProperty(document, ' visibilityState ' , { configurable:true,value: ' visible ' });window.dispatchEvent(new Event( ' visibilitychange ' ));delete document.visibilityState;});await response;return true; " )
self . settle ( )
retained = self . code ( " return await page.evaluate(()=>( { sameCanvas:window.__mapRetained===document.querySelector( ' [data-workplace-canvas] ' ),sameCamera:window.__mapCamera===document.querySelector( ' [data-workplace-canvas] .react-flow__viewport ' ).style.transform,nodes:document.querySelectorAll( ' [data-workplace-map] article ' ).length})); " )
self . assertTrue ( retained [ ' sameCanvas ' ] )
self . assertTrue ( retained [ ' sameCamera ' ] )
self . assertEqual ( retained [ ' nodes ' ] , 5 )
self . record ( ' refetch-retained ' , retained )
self . code ( " page.__mapSoftwareFailure=true;await page.evaluate(()=> { Object.defineProperty(document, ' visibilityState ' , { configurable:true,value: ' hidden ' });window.dispatchEvent(new Event( ' visibilitychange ' ));Object.defineProperty(document, ' visibilityState ' , { configurable:true,value: ' visible ' });window.dispatchEvent(new Event( ' visibilitychange ' ));delete document.visibilityState;});await page.getByRole( ' region ' , { name: ' Mapa visual del puesto ' ,exact:true}).getByRole( ' alert ' ).first().waitFor();return true; " )
self . assertEqual ( self . code ( " return await page.locator( ' [data-workplace-coverage=software] ' ).count(); " ) , 0 )
self . assertEqual ( self . code ( " return await page.locator( ' [data-workplace-map] article ' ).count(); " ) , 5 )
self . capture ( ' software-stale-no-total ' )
def test_contrast_and_reduced_motion ( self ) :
self . tool ( ' browser_resize ' , { ' width ' : 1440 , ' height ' : 900 } )
self . code ( " await page.emulateMedia( { reducedMotion: ' reduce ' });return true; " )
self . addCleanup ( self . code , " await page.emulateMedia( { reducedMotion: ' no-preference ' });return true; " )
self . map_ready ( )
values = self . code ( """ return await page.evaluate(()=> {
const canvas = document . createElement ( ' canvas ' ) ; canvas . width = canvas . height = 1 ; const ctx = canvas . getContext ( ' 2d ' , { willReadFrequently : true } ) ;
const rgba = color = > { ctx . clearRect ( 0 , 0 , 1 , 1 ) ; ctx . fillStyle = color ; ctx . fillRect ( 0 , 0 , 1 , 1 ) ; return [ . . . ctx . getImageData ( 0 , 0 , 1 , 1 ) . data ] . map ( ( x , i ) = > i == = 3 ? x / 255 : x ) } ;
const over = ( a , b ) = > { const alpha = a [ 3 ] + b [ 3 ] * ( 1 - a [ 3 ] ) ; return [ . . . a . slice ( 0 , 3 ) . map ( ( v , i ) = > ( v * a [ 3 ] + b [ i ] * b [ 3 ] * ( 1 - a [ 3 ] ) ) / alpha ) , alpha ] } ;
const bg = e = > e ? over ( rgba ( getComputedStyle ( e ) . backgroundColor ) , bg ( e . parentElement ) ) : [ 255 , 255 , 255 , 1 ] ;
const luminance = c = > c . slice ( 0 , 3 ) . map ( x = > x / 255 ) . map ( x = > x < = .04045 ? x / 12.92 : ( ( x + .055 ) / 1.055 ) * * 2.4 ) . reduce ( ( a , x , i ) = > a + x * [ .2126 , .7152 , .0722 ] [ i ] , 0 ) ;
const ratio = ( a , b ) = > { const l = [ luminance ( a ) , luminance ( b ) ] . sort ( ( a , b ) = > b - a ) ; return ( l [ 0 ] + .05 ) / ( l [ 1 ] + .05 ) } ;
const results = [ . . . document . querySelectorAll ( ' [data-workplace-map] article span,[data-workplace-map] .react-flow__node-softwareHeading span,aside[aria-label= " Leyenda " ] span ' ) ] . filter ( e = > e . innerText . trim ( ) & & ! e . classList . contains ( ' sr-only ' ) & & e . getBoundingClientRect ( ) . width ) . map ( e = > ( { text : e . innerText , contrast : ratio ( over ( rgba ( getComputedStyle ( e ) . color ) , bg ( e ) ) , bg ( e ) ) } ) ) ;
return { reducedMotion : matchMedia ( ' (prefers-reduced-motion: reduce) ' ) . matches , results } ;
} ) ; """ )
self . record ( ' contrast-and-motion ' , values )
self . assertTrue ( values [ ' reducedMotion ' ] )
for value in values [ ' results ' ] :
self . assertGreaterEqual ( value [ ' contrast ' ] , 4.5 , value [ ' text ' ] )
if __name__ == ' __main__ ' :
unittest . main ( defaultTest = [
' WorkplaceVisualMapTests.test_map ' ,
' WorkplaceVisualMapTests.test_dense ' ,
' WorkplaceVisualMapTests.test_navigation_and_management ' ,
' WorkplaceVisualMapTests.test_partial_errors_and_empty ' ,
' WorkplaceVisualMapTests.test_principal_only_and_resize ' ,
' WorkplaceVisualMapTests.test_software_coverage_states_and_refetch ' ,
' WorkplaceVisualMapTests.test_contrast_and_reduced_motion ' ,
] )