metis-desk/scripts/visual-audit-ui.test.py
David ba034794c6 feat: add column browser fixture and entity column configuration
- Introduced `ColumnBrowserFixture` component for testing column visibility and interactions.
- Created `entity-column-configuration.ts` to define types and utility functions for managing entity columns.
- Implemented Playwright tests in `columns-ui.test.py` to validate column behavior and preferences.
- Added `visual-audit-volume.py` script to enhance the visual audit setup with synthetic data.
2026-10-22 10:10:34 +02:00

939 lines
81 KiB
Python

#!/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.")
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.")
cls.output.mkdir(parents=True, exist_ok=True)
cls.resources = report.get("resources", [])
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;")
@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")
@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 evaluate(cls, expression):
return cls.code("return await page.evaluate(()=> (" + expression + "));")
@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 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;")
def test_selected_row_contrast(self):
self.resize(1920, 1080)
self.navigate("assets")
# Con datos voluminosos el activo de referencia puede quedar en otra página.
self.code("await page.locator('main input[data-quick-search]').fill('AUDIT-PORT-01');return true;")
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.getByRole('row').filter({has:page.getByRole('link',{name:'AUDIT-PORT-01',exact:true})}).evaluate(row=>{
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);
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 + "-1920")
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, [])
def test_mobile_navigation_geometry(self):
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:
self.resize(width,height)
self.navigate("assets")
self.code("await page.getByRole('button',{name:'Nuevo activo',exact:true}).waitFor();return true;")
self.settle()
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}};
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')};
});""")
self.record("navigation-geometry-"+str(width),metrics)
self.capture("navigation-assets-"+str(width))
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))
if width<768:
self.assertLessEqual(metrics['main']['y'],128)
if width==768:
self.assertEqual(metrics['main']['width'],768)
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.assertTrue(self.code("return await page.getByRole('dialog').isVisible();"))
self.resize(920,768)
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')
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)
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;")
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})
def inventory_metrics(self):
return self.code("""return await page.locator('[data-slot=inventory-layout]:has(table)').evaluate(root=>{const panel=root.querySelector(':scope>aside');const table=root.querySelector('table');const viewport=table.closest('[data-slot=table-container]');return {mode:root.dataset.detailMode,open:root.dataset.detailOpen,selected:root.dataset.selectedAsset??root.dataset.selectedResource,width:root.getBoundingClientRect().width,table:viewport.clientWidth,panel:panel?.getBoundingClientRect().width??0,gap:parseFloat(getComputedStyle(root).columnGap),dialogs:document.querySelectorAll('[role=dialog]').length,focus:document.activeElement.getAttribute('aria-label')||document.activeElement.textContent.trim().slice(0,60),bodyFocus:document.activeElement===document.body,inert:!!root.closest('[inert]'),sameTable:!window.inventoryTable||table===window.inventoryTable,url:location.href}});""")
def inventory_select(self):
self.code("await page.getByRole('row').filter({has:page.getByRole('link',{name:'AUDIT-PORT-01',exact:true})}).locator('td').nth(2).click();return true;")
self.settle()
def inventory_width(self, width):
self.code("await page.locator('[data-slot=inventory-layout]').evaluate((e,w)=>e.style.width=w+'px',"+str(width)+");return true;")
self.settle()
def test_inventory_container_capacity(self):
self.resize(1920,1080)
self.navigate('assets')
self.code("await page.locator('main input[data-quick-search]').fill('AUDIT-PORT-01');return true;")
self.code("await page.getByRole('link',{name:'AUDIT-PORT-01',exact:true}).waitFor();await page.evaluate(()=>window.inventoryTable=document.querySelector('main table'));return true;")
for width, mode in [(1143,'overlay'),(1144,'inline'),(1145,'inline'),(1224,'inline'),(1200,'inline'),(1143,'overlay')]:
self.inventory_width(width)
value=self.inventory_metrics()
self.assertEqual(value['mode'],mode)
self.assertEqual(value['width'],width)
self.assertTrue(value['sameTable'])
self.assertEqual(value['dialogs'],0)
if not value.get('selected'):
self.inventory_select()
elif value['open']=='false':
self.code("await page.getByRole('button',{name:'Mostrar detalle seleccionado',exact:true}).click();return true;")
self.settle()
value=self.inventory_metrics()
if mode=='inline':
self.assertGreaterEqual(value['table'],798) # Borde exterior de EntityTable: 2 px.
self.assertGreaterEqual(value['panel'],320)
self.assertLessEqual(value['panel'],400)
self.assertEqual(value['gap'],24)
self.assertEqual(value['dialogs'],0)
else:
self.assertEqual(value['panel'],0)
self.assertEqual(value['dialogs'],1)
self.record('inventory-boundary-'+str(width),value)
self.capture('inventory-boundary-'+str(width))
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.inventory_width(1144)
value=self.inventory_metrics()
self.assertEqual(value['open'],'false')
self.assertEqual(value['panel'],0)
self.assertEqual(value['dialogs'],0)
self.assertTrue(value.get('selected'))
self.code("await page.locator('[data-slot=inventory-layout]').evaluate(e=>e.style.removeProperty('width'));return true;")
def test_inventory_resize_focus_and_actions(self):
self.resize(1920,1080)
self.navigate('assets')
self.code("await page.getByRole('link',{name:'AUDIT-PORT-01',exact:true}).waitFor();await page.evaluate(()=>window.inventoryTable=document.querySelector('main table'));return true;")
self.settle()
initial=self.inventory_metrics()
self.record('inventory-focus-initial',initial)
self.assertEqual(initial['mode'],'inline',initial)
self.inventory_select()
self.assertGreater(self.inventory_metrics()['panel'],0)
self.code("await page.locator('[data-slot=inventory-layout]>aside').getByRole('button',{name:'Editar',exact:true}).focus();return true;")
selected=self.inventory_metrics()['selected']
self.resize(1100,800)
value=self.inventory_metrics()
self.assertEqual(value['selected'],selected)
self.assertEqual(value['focus'],'Mostrar detalle seleccionado')
self.assertEqual(value['dialogs'],0)
self.assertTrue(value['sameTable'])
self.code("await page.keyboard.press('Enter');await page.getByRole('dialog',{name:'AUDIT-PORT-01',exact:true}).waitFor();return true;")
self.resize(1920,1080)
self.code("await page.waitForFunction(()=>document.activeElement?.closest('[data-slot=inventory-layout]>aside'));return true;")
value=self.inventory_metrics()
self.assertEqual(value['dialogs'],0)
self.assertFalse(value['inert'])
self.assertFalse(value['bodyFocus'])
self.assertTrue(value['sameTable'])
self.record('inventory-overlay-to-inline-focus',value)
self.capture('inventory-overlay-to-inline-focus')
self.code("await page.getByRole('row').filter({has:page.getByRole('link',{name:'AUDIT-PORT-01',exact:true})}).getByRole('button',{name:'Editar',exact:true}).focus();await page.evaluate(()=>window.inventoryAction=document.activeElement);return true;")
self.resize(1100,800)
self.assertTrue(self.evaluate('document.activeElement===window.inventoryAction'))
self.assertEqual(self.inventory_metrics()['dialogs'],0)
self.code("await page.keyboard.press('Enter');await page.getByRole('dialog',{name:'Editar activo',exact:true}).waitFor();return true;")
self.capture('inventory-edit-after-resize')
self.code("await page.getByRole('dialog').getByRole('combobox',{name:'Sede',exact:true}).click();await page.getByRole('listbox').waitFor();return true;")
self.capture('inventory-edit-nested-select')
self.code("await page.keyboard.press('Escape');await page.getByRole('listbox').waitFor({state:'hidden'});return true;")
self.code("await page.getByRole('dialog').getByRole('button',{name:'Cancelar',exact:true}).click();await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.assertTrue(self.evaluate('document.activeElement===window.inventoryAction'))
self.code("await page.getByRole('button',{name:'Mostrar detalle seleccionado',exact:true}).click();return true;")
self.settle()
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});await page.waitForFunction(()=>document.activeElement.textContent==='Mostrar detalle seleccionado');return true;")
self.resize(1920,1080)
self.assertEqual(self.inventory_metrics()['panel'],0)
self.code("await page.getByRole('button',{name:'Mostrar detalle seleccionado',exact:true}).click();await page.locator('[data-slot=inventory-layout]>aside').getByRole('button',{name:'Ficha e historial',exact:true}).click();await page.waitForURL('**/assets/*');return true;")
self.assertEqual(self.code("return await page.getByRole('dialog').count();"),0)
self.capture('inventory-full-detail-after-panel')
def test_inventory_shell_capacity_changes(self):
self.resize(1660,900)
self.navigate('assets')
self.code("await page.getByRole('link',{name:'AUDIT-PORT-01',exact:true}).waitFor();return true;")
self.assertEqual(self.inventory_metrics()['mode'],'overlay')
self.code("await page.getByRole('button',{name:'Contraer menú',exact:true}).click();return true;")
self.settle()
self.assertEqual(self.inventory_metrics()['mode'],'inline')
self.inventory_select()
self.code("await page.getByRole('button',{name:'Expandir menú',exact:true}).click();return true;")
self.settle()
self.assertEqual(self.inventory_metrics()['mode'],'overlay')
self.assertEqual(self.inventory_metrics()['dialogs'],0)
self.resize(1366,768)
self.code("await page.getByRole('button',{name:'Ocultar secciones',exact:true}).click();return true;")
self.settle()
self.assertEqual(self.inventory_metrics()['mode'],'inline')
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'});return true;")
self.settle()
value=self.inventory_metrics()
self.assertEqual(value['mode'],'overlay')
self.assertEqual(value['dialogs'],0)
self.record('inventory-shell-capacity-changes',value)
self.code("await page.evaluate(()=>localStorage.removeItem('metis-desk:sidebar-collapsed'));return true;")
def test_inventory_preserves_table_state_and_scroll(self):
self.resize(1920,1080)
self.code("""await page.route('**/api/assets/*',async route=>{if(route.request().method()!=='GET')return route.fallback();const response=await route.fetch();const data=await response.json();if(data.assets?.length===1){const asset=data.assets[0];data.assets=[asset,...Array.from({length:40},(_,i)=>({...asset,id:crypto.randomUUID(),values:{...asset.values,name:'AUDIT-PORT-'+String(i+2).padStart(2,'0')}}))]}await route.fulfill({response,json:data})});return true;""")
try:
self.navigate('assets')
self.inventory_width(1144)
self.inventory_select()
self.code("await page.getByRole('tab',{name:'Portátil',exact:true}).click();await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('AUDIT');await page.getByRole('columnheader').getByRole('button',{name:/^Nombre/}).click();await page.getByRole('button',{name:'Página siguiente',exact:true}).click();await page.getByRole('button',{name:'Columnas',exact:true}).click();const d=page.getByRole('dialog',{name:'Columnas',exact:true});for(const c of await d.getByRole('checkbox').all()){if(await c.isEnabled())await c.check()}await page.keyboard.press('Escape');await d.waitFor({state:'hidden'});return true;")
self.settle()
self.code("await page.evaluate(()=>{window.inventoryTable=document.querySelector('main table');const sc=window.inventoryTable.closest('[data-slot=table-container]');sc.scrollLeft=150;const root=document.querySelector('[data-slot=workspace-scroll]');root.scrollTop=160;window.inventoryScroll={sc,root,x:sc.scrollLeft,y:root.scrollTop};window.inventoryRows=[...window.inventoryTable.querySelectorAll('tbody tr')].map(r=>r.textContent);window.inventoryColumns=localStorage.getItem('asset-inventory-columns')});return true;")
before=self.evaluate("{x:window.inventoryScroll.x,y:window.inventoryScroll.y,sort:document.querySelector('th[aria-sort]')?.getAttribute('aria-sort'),page:document.querySelector('main [aria-current=page]')?.textContent}")
self.assertGreater(before['x'],0)
self.assertGreater(before['y'],0)
for width in [1143,1144,1145,1143]:
self.inventory_width(width)
# El resize no vuelve a abrir el detalle cerrado en la primera pérdida de capacidad.
value=self.evaluate("{same:document.querySelector('main table')===window.inventoryTable,x:window.inventoryScroll.sc.scrollLeft,y:window.inventoryScroll.root.scrollTop,rows:JSON.stringify([...window.inventoryTable.querySelectorAll('tbody tr')].map(r=>r.textContent))===JSON.stringify(window.inventoryRows),columns:localStorage.getItem('asset-inventory-columns')===window.inventoryColumns,search:document.querySelector('input[data-quick-search]').value,sort:document.querySelector('th[aria-sort]')?.getAttribute('aria-sort'),page:document.querySelector('main [aria-current=page]')?.textContent,tab:document.querySelector('[role=tab][aria-selected=true]').textContent,dialogs:document.querySelectorAll('[role=dialog]').length}")
self.assertTrue(value['same'])
self.assertTrue(value['rows'])
self.assertTrue(value['columns'])
self.assertEqual(value['search'],'AUDIT')
self.assertEqual(value['sort'],before['sort'])
self.assertEqual(value['page'],before['page'])
self.assertEqual(value['tab'],'Portátil')
self.assertEqual(value['x'],before['x'])
self.assertEqual(value['y'],before['y'])
self.assertEqual(value['dialogs'],0)
self.record('inventory-table-state-'+str(width),value)
self.capture('inventory-table-state-scroll')
finally:
self.code("await page.unroute('**/api/assets/*');await page.evaluate(()=>{localStorage.removeItem('asset-inventory-columns');document.querySelector('[data-slot=inventory-layout]')?.style.removeProperty('width');delete window.inventoryTable});return true;")
def test_inventory_missing_selection_and_native_link(self):
self.resize(1920,1080)
self.navigate('assets')
self.inventory_select()
self.code("await page.getByRole('link',{name:'AUDIT-PORT-01',exact:true}).click();await page.waitForURL('**/assets/*');return true;")
self.assertEqual(self.code("return await page.getByRole('dialog').count();"),0)
self.navigate('assets')
self.inventory_select()
# Simula desaparición externa en respuestas HTTP; no borra el activo de la instalación.
self.code("await page.route('**/api/assets/*',async route=>{const response=await route.fetch();const data=await response.json();if(data.assets)data.assets=[];await route.fulfill({response,json:data})});await page.evaluate(()=>{window.dispatchEvent(new Event('visibilitychange'))});return true;")
try:
self.code("await page.getByText('No se ha encontrado el registro en este ámbito.',{exact:true}).waitFor({timeout:5000});return true;")
self.assertEqual(self.code("return await page.getByRole('dialog').count();"),0)
self.assertFalse(self.evaluate('document.activeElement===document.body'))
self.capture('inventory-selection-disappeared')
finally:
self.code("await page.unroute('**/api/assets/*');return true;")
def resource_select(self, name='AUDIT-APP-01', source='row'):
link="page.getByRole('link',{name:"+json.dumps(name)+",exact:true})"
self.code("await "+link+".waitFor();"+("await "+link+".click();" if source=='name' else "await page.getByRole('row').filter({has:"+link+"}).locator('td').nth(1).click();")+"return true;")
self.settle()
def test_resource_capacity_and_visibility(self):
self.resize(1920,1080)
self.navigate('resources')
self.code("await page.getByRole('link',{name:'AUDIT-APP-01',exact:true}).waitFor();await page.evaluate(()=>window.inventoryTable=document.querySelector('main table'));return true;")
try:
for width,mode in [(983,'overlay'),(984,'inline'),(985,'inline'),(1064,'inline')]:
self.inventory_width(width)
value=self.inventory_metrics()
if not value.get('selected'):
self.resource_select()
value=self.inventory_metrics()
self.assertEqual(value['mode'],mode)
self.assertEqual(value['width'],width)
self.assertTrue(value['sameTable'])
self.assertEqual(value['dialogs'],1 if mode=='overlay' else 0)
if mode=='inline':
self.assertGreaterEqual(value['table'],638)
self.assertGreaterEqual(value['panel'],320)
self.assertLessEqual(value['panel'],400)
self.assertEqual(value['gap'],24)
self.record('resource-capacity-'+str(width),value)
self.capture('resource-capacity-'+str(width))
self.code("await page.locator('[data-slot=selection-panel]').getByRole('button',{name:'Editar',exact:true}).focus();return true;")
self.inventory_width(983)
value=self.inventory_metrics()
self.assertEqual(value['dialogs'],0)
self.assertEqual(value['open'],'false')
self.assertEqual(value['focus'],'Mostrar detalle seleccionado')
self.assertTrue(value.get('selected'))
self.inventory_width(984)
self.assertEqual(self.inventory_metrics()['panel'],0)
self.code("await page.getByRole('button',{name:'Mostrar detalle seleccionado',exact:true}).click();return true;")
self.settle()
self.assertEqual(self.inventory_metrics()['panel'],320)
self.code("await page.getByRole('row').filter({has:page.getByRole('link',{name:'AUDIT-APP-01',exact:true})}).getByRole('button',{name:'Editar',exact:true}).focus();await page.evaluate(()=>window.resourceAction=document.activeElement);return true;")
self.inventory_width(983)
self.assertTrue(self.evaluate('document.activeElement===window.resourceAction'))
finally:
self.code("await page.locator('[data-slot=inventory-layout]').evaluate(e=>e.style.removeProperty('width'));return true;")
self.resize(1660,900)
self.code("await page.getByRole('button',{name:'Contraer menú',exact:true}).click();return true;")
self.settle()
self.assertTrue(self.inventory_metrics()['sameTable'])
self.assertEqual(self.inventory_metrics()['dialogs'],0)
self.code("await page.getByRole('button',{name:'Expandir menú',exact:true}).click();await page.evaluate(()=>localStorage.removeItem('metis-desk:sidebar-collapsed'));return true;")
self.resize(1160,800)
self.code("await page.getByRole('button',{name:'Ocultar secciones',exact:true}).click();return true;")
self.settle()
self.assertEqual(self.inventory_metrics()['mode'],'inline')
self.assertEqual(self.inventory_metrics()['panel'],0)
self.code("await page.getByRole('button',{name:'Mostrar detalle seleccionado',exact:true}).click();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'});return true;")
self.settle()
value=self.inventory_metrics()
self.assertEqual(value['mode'],'overlay')
self.assertEqual(value['dialogs'],0)
self.assertTrue(value['sameTable'])
self.record('resource-shell-capacity-changes',value)
def test_resource_routes_and_explicit_detail(self):
self.resize(1920,1080)
self.navigate('resources')
self.code("await page.getByRole('link',{name:'AUDIT-APP-01',exact:true}).waitFor();await page.evaluate(()=>window.inventoryTable=document.querySelector('main table'));return true;")
self.resource_select(source='name')
self.assertTrue(self.inventory_metrics()['sameTable'])
selected=self.inventory_metrics()['selected']
path=self.evaluate('location.pathname')
self.assertTrue(path.endswith('/'+selected))
self.code("await page.locator('[data-slot=selection-panel]').getByRole('button',{name:'Ficha e historial',exact:true}).click();await page.getByRole('dialog',{name:'AUDIT-APP-01',exact:true}).waitFor();return true;")
self.assertEqual(self.code("return await page.locator('[data-slot=selection-panel]').count();"),0)
self.assertEqual(self.evaluate('location.pathname'),path)
self.capture('resource-explicit-dialog-1920')
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.settle()
self.assertGreater(self.inventory_metrics()['panel'],0)
self.assertFalse(self.inventory_metrics()['bodyFocus'])
self.resize(1100,800)
self.assertEqual(self.inventory_metrics()['dialogs'],0)
self.code("await page.getByRole('button',{name:'Mostrar detalle seleccionado',exact:true}).focus();await page.keyboard.press('Enter');await page.getByRole('dialog').waitFor();return true;")
self.resize(1920,1080)
self.code("await page.waitForFunction(()=>document.activeElement.closest('[data-slot=selection-panel]'));return true;")
value=self.inventory_metrics()
self.assertEqual(value['dialogs'],0)
self.assertFalse(value['inert'])
self.record('resource-overlay-to-inline',value)
self.capture('resource-overlay-to-inline-focus')
self.resize(1100,800)
self.navigate('resources')
self.code("await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('AUDIT');return true;")
self.resource_select(source='name')
self.assertEqual(self.inventory_metrics()['dialogs'],1)
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});await page.waitForURL('**/resources?q=AUDIT');return true;")
self.assertFalse(self.inventory_metrics().get('selected'))
self.code("await page.goForward();await page.getByRole('dialog',{name:'AUDIT-APP-01',exact:true}).waitFor();return true;")
self.assertEqual(self.inventory_metrics()['selected'],selected)
self.capture('resource-forward-contextual-dialog')
def test_resource_state_and_lifecycle(self):
self.resize(1920,1080)
self.navigate('resources')
self.resource_select()
self.code("await page.getByRole('tab',{name:'Aplicación',exact:true}).click();await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('AUDIT-APP');await page.getByRole('columnheader').getByRole('button',{name:/^Nombre/}).click();await page.getByRole('button',{name:'Página siguiente',exact:true}).click();await page.getByRole('button',{name:'Columnas',exact:true}).click();await page.getByRole('dialog').getByRole('checkbox',{name:'Estado',exact:true}).uncheck();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.inventory_width(984)
self.settle()
before=self.code("return await page.evaluate(()=>{window.resourceTable=document.querySelector('main table');window.resourceScroll=document.querySelector('[data-slot=workspace-scroll]');window.resourceScroll.scrollTop=100;return {y:window.resourceScroll.scrollTop,url:location.href,rows:[...window.resourceTable.querySelectorAll('tbody tr')].map(e=>e.textContent),headers:[...window.resourceTable.querySelectorAll('th')].map(e=>e.textContent),page:document.querySelector('main [aria-current=page]').textContent}});")
for width in [983,984,985]:
self.inventory_width(width)
value=self.evaluate("{same:document.querySelector('main table')===window.resourceTable,y:window.resourceScroll.scrollTop,url:location.href,rows:[...window.resourceTable.querySelectorAll('tbody tr')].map(e=>e.textContent),headers:[...window.resourceTable.querySelectorAll('th')].map(e=>e.textContent),page:document.querySelector('main [aria-current=page]').textContent,search:document.querySelector('input[data-quick-search]').value,tab:document.querySelector('[role=tab][aria-selected=true]').textContent}")
self.assertTrue(value['same'])
for key in ['url','rows','headers','page']:
self.assertEqual(value[key],before[key])
self.assertEqual(value['search'],'AUDIT-APP')
self.assertEqual(value['tab'],'Aplicación')
self.record('resource-state-'+str(width),value)
self.capture('resource-filtered-page-and-columns')
self.resize(1920,768)
self.code("await page.getByRole('button',{name:'Página anterior',exact:true}).click();await page.getByRole('button',{name:'Mostrar detalle seleccionado',exact:true}).click();return true;")
self.inventory_width(984)
self.code("await page.evaluate(()=>document.querySelector('[data-slot=workspace-scroll]').scrollTop=50);return true;")
self.assertEqual(self.evaluate("document.querySelector('[data-slot=workspace-scroll]').scrollTop"),50)
self.inventory_width(983)
self.assertEqual(self.evaluate("document.querySelector('[data-slot=workspace-scroll]').scrollTop"),50)
self.record('resource-vertical-scroll-preserved',self.inventory_metrics())
self.code("await page.getByRole('button',{name:'Página siguiente',exact:true}).click();await page.getByRole('button',{name:'Columnas',exact:true}).click();await page.getByRole('dialog').getByRole('checkbox',{name:'Estado',exact:true}).check();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});await page.locator('[data-slot=inventory-layout]').evaluate(e=>e.style.removeProperty('width'));return true;")
self.resize(320,640)
self.code("await page.evaluate(()=>{const sc=document.querySelector('main table').closest('[data-slot=table-container]');sc.scrollLeft=10;window.resourceX=sc.scrollLeft});return true;")
self.assertGreater(self.evaluate('window.resourceX'),0)
self.resize(321,640)
self.assertEqual(self.evaluate("document.querySelector('main table').closest('[data-slot=table-container]').scrollLeft"),10)
self.assertFalse(self.evaluate('document.documentElement.scrollWidth>innerWidth'))
self.resize(1920,1080)
self.code("const row=page.getByRole('row').filter({has:page.getByRole('link',{name:'AUDIT-APP-01',exact:true})});await row.getByRole('button',{name:'Acciones',exact:true}).click();return true;")
self.assertTrue(self.code("return await page.getByRole('menuitem',{name:'Desactivar recurso',exact:true}).isVisible();"))
self.code("await page.getByRole('menuitem',{name:'Eliminar definitivamente',exact:true}).click();await page.getByRole('dialog').waitFor();await page.getByRole('dialog').getByRole('button',{name:'Cancelar',exact:true}).click();await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.assertFalse(self.evaluate('document.activeElement===document.body'))
def test_resource_editor_focus_and_nested(self):
self.resize(1920,1080)
self.navigate('resources')
self.code("await page.getByRole('tab',{name:'Licencia de software',exact:true}).click();return true;")
self.resource_select('AUDIT-LICENSE — Suite empresarial de pruebas')
self.code("await page.locator('[data-slot=selection-panel]').getByRole('button',{name:'Editar',exact:true}).click();await page.getByRole('dialog').getByRole('textbox',{name:/^Nombre/}).fill('Borrador de recurso sin guardar');await page.getByRole('dialog').getByRole('combobox',{name:'Modalidad de licencia',exact:true}).click();return true;")
self.resize(1100,800)
self.assertEqual(self.code("return await page.getByRole('dialog').getByRole('textbox',{name:/^Nombre/}).inputValue();"),'Borrador de recurso sin guardar')
self.capture('resource-editor-resize-with-select')
self.code("if(await page.getByRole('listbox').count()){await page.keyboard.press('Escape');await page.getByRole('listbox').waitFor({state:'hidden'})}await page.getByRole('dialog').getByRole('button',{name:'Cancelar',exact:true}).click();await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.settle()
value=self.inventory_metrics()
self.assertEqual(value['dialogs'],0)
self.assertEqual(value['focus'],'Mostrar detalle seleccionado')
self.record('resource-editor-removed-trigger',value)
self.navigate('resources')
self.code("await page.getByRole('row').filter({has:page.getByRole('link',{name:'AUDIT-APP-01',exact:true})}).getByRole('button',{name:'Editar',exact:true}).focus();await page.evaluate(()=>window.resourceEdit=document.activeElement);await page.keyboard.press('Enter');await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.assertTrue(self.evaluate('document.activeElement===window.resourceEdit'))
def test_resource_locale_and_related(self):
name=next(row['name'] for row in self.resources if row['kind']=='shared_folder')
self.resize(1920,1080)
self.navigate('resources')
self.code("await page.getByRole('tab',{name:'Carpeta compartida',exact:true}).click();return true;")
self.resource_select(name,source='name')
self.assertEqual(self.code("return await page.locator('[data-slot=selection-panel]').getAttribute('aria-label');"),'Ficha del recurso: '+name)
self.code("await page.locator('[data-slot=selection-panel] a[href*=\"/access?\"]').first().click();await page.waitForURL('**/access?**');return true;")
self.capture('resource-related-existing-route')
self.code("await page.goBack();await page.locator('[data-slot=selection-panel]').waitFor();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.locator('summary').click();return true;")
try:
self.assertEqual(self.code("return await page.locator('[data-slot=selection-panel]').getAttribute('aria-label');"),'Resource details: '+name)
self.capture('resource-long-name-english-inline')
for width,height in [(390,844),(320,640)]:
self.resize(width,height)
self.code("await page.getByRole('button',{name:'Show selected details',exact:true}).click();await page.getByRole('dialog').waitFor();return true;")
self.assertFalse(self.evaluate('document.documentElement.scrollWidth>innerWidth'))
self.capture('resource-long-name-english-'+str(width))
self.resize(1920,1080)
self.code("await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
finally:
self.code("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;")
def test_resource_external_disappearance(self):
self.resize(1920,1080)
self.navigate('resources')
self.resource_select()
selected=self.inventory_metrics()['selected']
self.code("await page.getByRole('row').filter({has:page.getByRole('link',{name:'AUDIT-APP-02',exact:true})}).getByRole('button',{name:'Acciones',exact:true}).click();await page.getByRole('menuitem',{name:'Ficha e historial',exact:true}).click();await page.getByRole('dialog',{name:'AUDIT-APP-02',exact:true}).waitFor();return true;")
# Solo transforma la respuesta leída por este navegador; no elimina datos del entorno.
self.code("await page.route('**/api/resources/*',async route=>{const response=await route.fetch();const data=await response.json();if(data.resources)data.resources=data.resources.filter(r=>r.values.name!=='AUDIT-APP-02');await route.fulfill({response,json:data})});await page.evaluate(()=>window.dispatchEvent(new Event('visibilitychange')));return true;")
try:
self.code("await page.getByRole('dialog').waitFor({state:'hidden'});await page.locator('[data-slot=selection-panel]').waitFor();return true;")
self.settle()
self.assertEqual(self.inventory_metrics()['selected'],selected)
self.assertFalse(self.evaluate('document.activeElement===document.body'))
self.assertTrue(self.evaluate("Boolean(document.activeElement.closest('[data-slot=selection-panel]'))"))
self.capture('resource-explicit-view-disappeared')
self.code("await page.unroute('**/api/resources/*');await page.route('**/api/resources/*',async route=>{const response=await route.fetch();const data=await response.json();if(data.resources)data.resources=[];await route.fulfill({response,json:data})});await page.evaluate(()=>window.dispatchEvent(new Event('visibilitychange')));return true;")
self.code("await page.getByText('No se ha encontrado el registro en este ámbito.',{exact:true}).waitFor();return true;")
self.assertEqual(self.code("return await page.getByRole('dialog').count();"),0)
self.assertFalse(self.evaluate('document.activeElement===document.body'))
self.assertTrue(self.evaluate("document.activeElement===document.querySelector('[data-slot=inventory-layout]')"))
self.capture('resource-selected-disappeared')
finally:
self.code("await page.unroute('**/api/resources/*');return true;")
def test_resource_observer_cleanup(self):
self.code("""await page.addInitScript(()=>{const Native=window.ResizeObserver;window.resourceObservers={active:0,observed:0,disconnected:0};window.ResizeObserver=class extends Native{observe(target,options){if(target.dataset.slot==='inventory-layout'&&!this.inventory){this.inventory=true;window.resourceObservers.active++;window.resourceObservers.observed++}super.observe(target,options)}disconnect(){if(this.inventory){this.inventory=false;window.resourceObservers.active--;window.resourceObservers.disconnected++}super.disconnect()}}});return true;""")
self.resize(1920,1080)
self.navigate('resources')
self.code("await page.getByRole('link',{name:'AUDIT-APP-01',exact:true}).waitFor();return true;")
self.settle()
self.assertEqual(self.evaluate('window.resourceObservers.active'),1)
self.inventory_width(984)
self.code("await page.evaluate(()=>{window.resourceModes=[];window.resourceModeObserver=new MutationObserver(ms=>{for(const m of ms){if(m.attributeName==='data-detail-mode')window.resourceModes.push(m.target.dataset.detailMode)}});window.resourceModeObserver.observe(document.querySelector('[data-slot=inventory-layout]'),{attributes:true})});return true;")
for width in [985,986,1000]:
self.inventory_width(width)
self.assertEqual(self.evaluate('window.resourceModes'),[])
self.assertEqual(self.evaluate('window.resourceObservers.active'),1)
self.code("await page.locator('[data-company-fixed-navigation] a').filter({hasText:'Resumen'}).click();return true;")
self.settle()
self.assertEqual(self.evaluate('window.resourceObservers.active'),0)
self.assertGreaterEqual(self.evaluate('window.resourceObservers.disconnected'),1)
self.code("await page.locator("+json.dumps('[data-company-fixed-navigation] a[href="'+self.company+'/resources"]')+").click();await page.getByRole('link',{name:'AUDIT-APP-01',exact:true}).waitFor();return true;")
self.settle()
self.assertEqual(self.evaluate('window.resourceObservers.active'),1)
self.record('resource-observer-cleanup',self.evaluate('window.resourceObservers'))
if __name__ == "__main__":
unittest.main()