- Added `useCompanyNavigationEntries` for defining navigation entries. - Created `CompanyContextBar` for displaying company information and navigation. - Developed `CompanyHeader` for showing company identity and status. - Introduced `CompanyNavigationSheet` for mobile navigation options. - Implemented `CompanySectionNavigation` for section-specific navigation. - Added `workspaceContentFocusTarget` to manage focus behavior. - Created `WorkspaceMobileNavigationProvider` for managing mobile navigation state. - Developed `WorkspaceMobileSheet` for consistent mobile sheet UI. - Added `WorkspaceSkipLink` for accessibility improvements. - Established `WorkspaceMobileNavigationContext` for navigation state management. - Created `WorkspaceNavigationContent` for displaying workspace navigation. - Implemented `WorkspaceSearchContent` and `WorkspaceSearch` for search functionality.
375 lines
31 KiB
Python
375 lines
31 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.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 record(cls, name, value):
|
|
cls.results.append({"scenario": name, "value": value})
|
|
(cls.output / "measurements.json").write_text(json.dumps(cls.results, ensure_ascii=False, indent=2) + "\n")
|
|
|
|
def navigate(self, section):
|
|
self.tool("browser_navigate", {"url": self.origin + self.company + "/" + section})
|
|
self.code("await page.locator('[data-slot=workspace]').waitFor(); return true;")
|
|
|
|
def capture(self, name):
|
|
AUDIT["capture"](self.client, self.output, name)
|
|
result = self.tool("browser_snapshot", {})
|
|
(self.output / (name + ".txt")).write_text("\n".join(p["text"] for p in result["content"] if p.get("type") == "text"))
|
|
|
|
def resize(self, width, height):
|
|
self.tool("browser_resize", {"width": width, "height": height})
|
|
self.settle()
|
|
|
|
def settle(self):
|
|
self.code("await page.evaluate(async()=>{await document.fonts.ready; await Promise.all(document.getAnimations().filter(a=>a.effect?.getTiming().iterations!==Infinity).map(a=>a.finished.catch(()=>{})))}); return true;")
|
|
|
|
def test_selected_row_contrast(self):
|
|
self.resize(1440, 900)
|
|
self.navigate("assets")
|
|
self.code("await page.getByRole('link',{name:'AUDIT-PORT-01',exact:true}).waitFor();return true;")
|
|
values = []
|
|
for state in ("normal", "normal-hover", "normal-focus", "selected", "hover", "focus"):
|
|
if state == "selected":
|
|
self.code("await page.getByRole('row').filter({has:page.getByRole('link',{name:'AUDIT-PORT-01',exact:true})}).locator('td').nth(2).click();return true;")
|
|
if state.endswith("hover"):
|
|
self.code("await page.getByRole('link',{name:'AUDIT-PORT-01',exact:true}).hover(); return true;")
|
|
if state.endswith("focus"):
|
|
self.code("await page.mouse.move(0,0); await page.getByRole('link',{name:'AUDIT-PORT-01',exact:true}).focus(); await page.keyboard.press('Tab'); await page.keyboard.press('Shift+Tab'); return true;")
|
|
self.settle()
|
|
measurements = self.code("""return await page.evaluate(()=>{
|
|
const rgb=c=>{const ctx=document.createElement('canvas').getContext('2d');ctx.fillStyle=c;ctx.fillRect(0,0,1,1);return [...ctx.getImageData(0,0,1,1).data].slice(0,3)};
|
|
const background=e=>{const colors=[];for(let n=e;n;n=n.parentElement)colors.unshift(getComputedStyle(n).backgroundColor);const ctx=document.createElement('canvas').getContext('2d');ctx.fillStyle='#fff';ctx.fillRect(0,0,1,1);for(const color of colors){ctx.fillStyle=color;ctx.fillRect(0,0,1,1)}return [...ctx.getImageData(0,0,1,1).data].slice(0,3)};
|
|
const lum=c=>c.map(v=>v/255).map(v=>v<=.04045?v/12.92:((v+.055)/1.055)**2.4).reduce((s,v,i)=>s+v*[.2126,.7152,.0722][i],0);
|
|
const row=[...document.querySelectorAll('tr')].find(e=>e.textContent.includes('AUDIT-PORT-01'));
|
|
return [...row.querySelectorAll('a,button,[class*=text-muted-foreground]')].map(e=>{const fg=rgb(getComputedStyle(e).color);const bg=background(e);const l=[lum(fg),lum(bg)].sort((a,b)=>a-b);return {label:e.textContent.trim()||e.getAttribute('aria-label'),fg,bg,ratio:(l[1]+.05)/(l[0]+.05)};});
|
|
});
|
|
""")
|
|
if state.endswith("focus"):
|
|
self.assertTrue(self.code("return await page.getByRole('link',{name:'AUDIT-PORT-01',exact:true}).evaluate(e=>e.matches(':focus-visible'));"))
|
|
values.extend(measurements)
|
|
self.record("contrast-" + state, measurements)
|
|
self.capture("contrast-" + state + "-1440")
|
|
self.assertTrue(all(x["ratio"] >= 4.5 for x in values), values)
|
|
|
|
def test_wizard_geometry(self):
|
|
failures = []
|
|
for section, title, field, template in [("assets", "Nuevo activo", "Plantilla", "Portátil"), ("resources", "Nuevo recurso", "Plantilla de recurso", "Aplicación")]:
|
|
for width, height in SIZES:
|
|
self.resize(width, height)
|
|
self.navigate(section)
|
|
self.code("await page.getByRole('button',{name:" + json.dumps(title) + ",exact:true}).click(); await page.getByRole('dialog').getByRole('combobox',{name:" + json.dumps(field) + ",exact:true}).click(); await page.getByRole('option',{name:" + json.dumps(template) + ",exact:true}).click(); return true;")
|
|
# Tras operar el desplegable, inspecciona el inicio del cuerpo desplazable.
|
|
self.code("await page.locator('[role=dialog] [data-slot=form-body]').evaluate(e=>e.scrollTo({top:0}));return true;")
|
|
geometry = []
|
|
for step in (1, 2, 3):
|
|
self.settle()
|
|
value = self.code("""return await page.evaluate(()=>{const d=document.querySelector('[role=dialog]');const r=d.getBoundingClientRect();const f=d.querySelector('[data-slot=dialog-footer]').getBoundingClientRect();const b=d.querySelector('[data-slot=form-body]');return {y:r.y,height:r.height,bottom:r.bottom,footerY:f.y,footerBottom:f.bottom,progressY:b.querySelector('ol').getBoundingClientRect().y,bodyClient:b.clientHeight,bodyScroll:b.scrollHeight};});""")
|
|
geometry.append(value)
|
|
self.capture(f"{section}-{width}-step-{step}")
|
|
if step < 3:
|
|
self.code("await page.getByRole('dialog').getByRole('button',{name:'Siguiente',exact:true}).click(); return true;")
|
|
self.record(f"geometry-{section}-{width}", geometry)
|
|
if max(x["height"] for x in geometry)-min(x["height"] for x in geometry)>1 or max(x["footerY"] for x in geometry)-min(x["footerY"] for x in geometry)>1:
|
|
failures.append(f"{section} {width}: altura o pie variables")
|
|
if max(x["progressY"] for x in geometry)-min(x["progressY"] for x in geometry)>1:
|
|
failures.append(f"{section} {width}: progreso desplazado")
|
|
if any(x["y"] < 0 or x["bottom"] > height+1 or x["footerBottom"] > height+1 for x in geometry):
|
|
failures.append(f"{section} {width}: fuera de viewport")
|
|
self.code("await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'}); return true;")
|
|
self.assertEqual(failures, [])
|
|
|
|
def test_focus_return(self):
|
|
self.resize(1440, 900)
|
|
failures = []
|
|
for origin, opening, closing in [("header", "mouse", "Escape"), ("header", "keyboard", "Escape"), ("header", "mouse", "Cancelar"), ("row", "keyboard", "Escape"), ("row", "mouse", "Cancelar"), ("missing", "keyboard", "Escape"), ("disabled", "mouse", "Escape")]:
|
|
self.navigate("assets")
|
|
label = "Editar" if origin == "row" else "Nuevo activo"
|
|
self.code("const trigger=page.getByRole('button',{name:" + json.dumps(label) + ",exact:true}); await trigger.waitFor();" + ("await trigger.focus();await page.keyboard.press('Enter');" if opening == "keyboard" else "await trigger.click();") + "await page.getByRole('dialog').waitFor();return true;")
|
|
if origin == "missing":
|
|
self.code("await page.locator('[data-slot=inventory-layout] button').filter({hasText:'Nuevo activo'}).evaluate(e=>e.remove());return true;")
|
|
if origin == "disabled":
|
|
self.code("await page.locator('[data-slot=inventory-layout] button').filter({hasText:'Nuevo activo'}).evaluate(e=>e.disabled=true);return true;")
|
|
self.code(("await page.keyboard.press('Escape');" if closing == "Escape" else "await page.getByRole('dialog').getByRole('button',{name:'Cancelar',exact:true}).click();") + "await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
|
|
self.settle()
|
|
result = self.code("await page.waitForFunction(()=>document.activeElement!==document.body,{},{timeout:2000}).catch(()=>{});return await page.evaluate(()=>({tag:document.activeElement.tagName,label:document.activeElement.getAttribute('aria-label')||document.activeElement.textContent.trim().slice(0,80),search:document.activeElement.hasAttribute('data-quick-search')}));")
|
|
self.record(f"focus-{origin}-{opening}-{closing}", result)
|
|
self.capture(f"focus-{origin}-{opening}-{closing}")
|
|
if (origin in ("missing", "disabled") and not result["search"]) or (origin not in ("missing", "disabled") and result["label"] != label):
|
|
failures.append([origin, opening, closing, result])
|
|
for width, height in SIZES[1:]:
|
|
self.resize(width, height)
|
|
self.navigate("assets")
|
|
self.code("await page.getByRole('button',{name:'Nuevo activo',exact:true}).focus();await page.keyboard.press('Enter');await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
|
|
self.settle()
|
|
target = self.code("return await page.evaluate(()=>document.activeElement.textContent.trim());")
|
|
self.record(f"focus-header-{width}", target)
|
|
self.capture(f"focus-header-{width}")
|
|
self.assertEqual(target, "Nuevo activo")
|
|
self.resize(1440, 900)
|
|
self.navigate("work-profiles")
|
|
self.code("await page.getByRole('button',{name:'Nuevo perfil',exact:true}).click(); await page.getByRole('button',{name:'Seleccionar puesto',exact:true}).click();return true;")
|
|
self.assertTrue(self.code("return await page.evaluate(()=>document.activeElement.hasAttribute('data-quick-search'));"))
|
|
self.code("await page.keyboard.press('Escape');return true;")
|
|
self.settle()
|
|
self.assertEqual(self.code("return await page.evaluate(()=>document.activeElement.textContent.trim());"), "Seleccionar puesto")
|
|
self.code("await page.keyboard.press('Escape');return true;")
|
|
self.assertEqual(failures, [])
|
|
|
|
def test_review_headers(self):
|
|
failures = []
|
|
self.resize(1440, 900)
|
|
for section in ("work-profiles", "folder-mappings", "application-configurations", "certificate-configurations", "printer-configurations", "mail-configurations"):
|
|
self.navigate(section)
|
|
self.code("await page.getByRole('table').waitFor();return true;")
|
|
headers = self.code("return await page.locator('th').allTextContents();")
|
|
self.record("headers-" + section, headers)
|
|
self.code("const header=page.getByRole('columnheader',{name:/Revisión/});if(await header.count())await header.scrollIntoViewIfNeeded();return true;")
|
|
self.capture("headers-" + section + "-1440")
|
|
if not any(x.strip().startswith("Revisión") for x in headers) or any("Revisar:" in x for x in headers):
|
|
failures.append(section)
|
|
for width, height in SIZES[1:]:
|
|
self.resize(width, height)
|
|
self.navigate("work-profiles")
|
|
self.code("await page.getByRole('table').waitFor();return true;")
|
|
headers = self.code("return await page.locator('th').allTextContents();")
|
|
self.record(f"headers-work-profiles-{width}", headers)
|
|
self.code("await page.getByRole('columnheader',{name:/Revisión/}).scrollIntoViewIfNeeded();return true;")
|
|
self.capture(f"headers-work-profiles-{width}")
|
|
self.assertTrue(any(x.strip().startswith("Revisión") for x in headers))
|
|
self.assertEqual(failures, [])
|
|
|
|
|
|
def test_mobile_navigation_geometry(self):
|
|
baseline_path = ROOT / ".artifacts/playwright/v01-1/before/measurements.json"
|
|
baseline = json.loads(baseline_path.read_text()) if baseline_path.exists() else []
|
|
for width, height in [(320,640),(390,844),(768,1024),(1024,768),(1366,768),(1440,900),(1920,1080)]:
|
|
self.resize(width,height)
|
|
self.navigate("assets")
|
|
self.code("await page.getByRole('button',{name:'Nuevo activo',exact:true}).waitFor();return true;")
|
|
metrics = self.code("""return await page.evaluate(()=>{
|
|
const r=e=>{const b=e.getBoundingClientRect();return {x:b.x,y:b.y,width:b.width,height:b.height}};
|
|
return {main:r(document.querySelector('main')),header:r(document.querySelector('header')),global:r(document.querySelector('[data-workspace-desktop-navigation]')),context:r(document.querySelector('[data-slot=company-context-bar]')),overflow:document.documentElement.scrollWidth>innerWidth,dialog:document.querySelectorAll('[role=dialog]').length,visibleSections:[...document.querySelectorAll('nav[aria-label="Información de la compañía"] a')].filter(e=>e.getClientRects().length>0).length};
|
|
});""")
|
|
self.record("navigation-geometry-"+str(width),metrics)
|
|
self.capture("navigation-assets-"+str(width))
|
|
self.assertFalse(metrics["overflow"])
|
|
self.assertEqual(metrics["dialog"],0)
|
|
if width<768:
|
|
self.assertLessEqual(metrics["main"]["y"],128)
|
|
self.assertEqual(metrics["global"]["width"],0)
|
|
self.assertEqual(metrics["visibleSections"],0)
|
|
else:
|
|
self.assertEqual(metrics["global"]["width"],228)
|
|
self.assertEqual(metrics["context"]["width"],0)
|
|
self.assertEqual(metrics["visibleSections"],21)
|
|
old=next((x["metrics"]["main"] for x in baseline if x["screen"]=="assets" and x["width"]==width),None)
|
|
if old:
|
|
self.assertAlmostEqual(metrics["main"]["width"],old["w"],delta=1)
|
|
self.assertAlmostEqual(metrics["main"]["y"],old["y"],delta=1)
|
|
|
|
def test_mobile_navigation_keyboard_and_routes(self):
|
|
self.resize(390,844)
|
|
self.navigate("assets")
|
|
self.code("await page.getByRole('button',{name:/^Secciones de /}).click();await page.getByRole('dialog').waitFor();return true;")
|
|
links=self.code("return await page.getByRole('dialog').locator('nav a').evaluateAll(es=>es.map(e=>({href:e.getAttribute('href'),label:e.textContent.trim(),current:e.getAttribute('aria-current')}))); ")
|
|
self.record("navigation-21-destinations",links)
|
|
self.assertEqual(len(links),21)
|
|
self.assertEqual(len({x['href'] for x in links}),21)
|
|
self.assertEqual(sum(x['current']=='page' for x in links),1)
|
|
self.capture("navigation-company-menu-390")
|
|
focused=self.code("""await page.getByRole('dialog').getByRole('button',{name:'Cerrar navegación'}).focus();const visited=[];for(let i=0;i<26;i++){await page.keyboard.press('Tab');await page.waitForFunction(()=>!!document.activeElement.closest('[role=dialog]'));visited.push(await page.evaluate(()=>({inside:!!document.activeElement.closest('[role=dialog]'),href:document.activeElement.getAttribute('href')})))}return visited;""")
|
|
self.assertTrue(all(x['inside'] for x in focused))
|
|
self.assertTrue(any(x['href']==links[-1]['href'] for x in focused))
|
|
reverse=self.code("""await page.getByRole('dialog').getByRole('button',{name:'Cerrar navegación'}).focus();const visited=[];for(let i=0;i<26;i++){await page.keyboard.press('Shift+Tab');await page.waitForFunction(()=>!!document.activeElement.closest('[role=dialog]'));visited.push(await page.evaluate(()=>({inside:!!document.activeElement.closest('[role=dialog]'),href:document.activeElement.getAttribute('href')})))}return visited;""")
|
|
self.record("navigation-keyboard-cycle",{"forward":focused,"reverse":reverse})
|
|
self.assertTrue(all(x['inside'] for x in reverse))
|
|
self.assertTrue(any(x['href']==links[-1]['href'] for x in reverse))
|
|
self.code("await page.getByRole('dialog').getByRole('link',{name:/^Personas/}).focus();await page.keyboard.press('Space');return true;")
|
|
self.assertTrue(self.code("return page.url().endsWith('/assets');"))
|
|
self.code("await page.keyboard.press('Enter');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
|
|
self.assertTrue(self.code("return page.url().endsWith('/people');"))
|
|
self.code("await page.waitForFunction(()=>!!document.activeElement.closest('main'));return true;")
|
|
self.assertTrue(self.code("return await page.evaluate(()=>document.activeElement.textContent.includes('Personas'));"))
|
|
self.code("await page.goBack();await page.getByRole('button',{name:'Nuevo activo',exact:true}).waitFor();await page.goForward();await page.getByRole('button',{name:'Nueva persona',exact:true}).waitFor();return true;")
|
|
self.assertTrue(self.code("return page.url().endsWith('/people');"))
|
|
self.navigate("assets")
|
|
self.code("await page.getByRole('button',{name:/^Secciones de /}).click();await page.getByRole('dialog').getByRole('button',{name:'Cambiar compañía'}).click();return true;")
|
|
switched=self.code("""await page.waitForFunction(()=>document.querySelector('[role=dialog]')?.textContent.includes('Menú global')&&!!document.activeElement.closest('[role=dialog]'));return await page.evaluate(()=>({dialogs:document.querySelectorAll('[role=dialog]').length,focusInside:!!document.activeElement.closest('[role=dialog]')}));""")
|
|
self.record("navigation-menu-transfer",switched)
|
|
self.assertEqual(switched,{"dialogs":1,"focusInside":True})
|
|
self.capture("navigation-global-menu-390")
|
|
self.assertTrue(self.code("return await page.getByRole('dialog').getByRole('link',{name:'Inicio',exact:true}).evaluate(e=>{const r=e.getBoundingClientRect();return r.top>=0&&r.bottom<=innerHeight;});"))
|
|
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
|
|
self.code("await page.waitForFunction(()=>document.activeElement.getAttribute('aria-label')?.startsWith('Secciones de '));return true;")
|
|
for button in ["Menú global","Buscar en Metis Desk"]:
|
|
self.code("await page.getByRole('button',{name:"+json.dumps(button)+",exact:true}).focus();await page.keyboard.press('Space');await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
|
|
self.code("await page.waitForFunction(label=>document.activeElement.getAttribute('aria-label')===label,"+json.dumps(button)+");return true;")
|
|
self.code("await page.getByRole('button',{name:/^Secciones de /}).click();return true;")
|
|
for click in [{"modifiers":["Control"]},{"button":"middle"}]:
|
|
result=self.code("""const popup=page.context().waitForEvent('page');await page.getByRole('dialog').getByRole('link',{name:/^Personas/}).click("""+json.dumps(click)+""");const other=await popup;await other.waitForLoadState('domcontentloaded');const result={path:new URL(other.url()).pathname,original:new URL(page.url()).pathname,stillOpen:await page.getByRole('dialog').isVisible()};await other.close();return result;""")
|
|
self.record("navigation-modified-click-"+str(click),result)
|
|
self.assertTrue(result['path'].endswith('/people'))
|
|
self.assertTrue(result['original'].endswith('/assets'))
|
|
self.assertTrue(result['stillOpen'])
|
|
self.code("await page.keyboard.press('Escape');return true;")
|
|
|
|
def test_mobile_navigation_search_sticky_and_resize(self):
|
|
self.resize(320,640)
|
|
self.navigate("assets")
|
|
self.code("await page.keyboard.press('Tab');await page.getByRole('link',{name:'Saltar al contenido'}).focus();await page.keyboard.press('Enter');return true;")
|
|
self.assertTrue(self.code("return await page.evaluate(()=>document.activeElement.id==='workspace-main');"))
|
|
self.code("await page.getByRole('button',{name:'Buscar en Metis Desk',exact:true}).click();await page.getByRole('dialog').getByRole('textbox').fill('Auditoría');return true;")
|
|
self.assertEqual(self.code("return await page.getByRole('dialog').getByRole('link').count();"),1)
|
|
self.capture("navigation-search-320")
|
|
self.code("await page.getByRole('dialog').getByRole('link').click();await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
|
|
self.assertEqual(self.code("return new URL(page.url()).pathname;"),self.company)
|
|
self.navigate("assets")
|
|
self.code("await page.getByRole('link',{name:'AUDIT-PORT-01',exact:true}).click();await page.locator('main h1').filter({hasText:'AUDIT-PORT-01'}).waitFor();await page.locator('[data-slot=workspace-scroll]').evaluate(e=>e.scrollTop=800);return true;")
|
|
self.settle()
|
|
sticky=self.code("""return await page.evaluate(()=>({header:document.querySelector('header').getBoundingClientRect().y,context:document.querySelector('[data-slot=company-context-bar]').getBoundingClientRect().y,scroll:document.querySelector('[data-slot=workspace-scroll]').scrollTop}));""")
|
|
self.record("navigation-sticky-320",sticky)
|
|
self.assertGreater(sticky['scroll'],0)
|
|
self.assertEqual(sticky['header'],0)
|
|
self.assertAlmostEqual(sticky['context'],56,delta=2)
|
|
self.capture("navigation-sticky-320")
|
|
self.code("await page.locator('main button').first().focus();return true;")
|
|
position=self.code("return await page.evaluate(()=>document.activeElement.getBoundingClientRect().top);")
|
|
self.assertGreaterEqual(position,104)
|
|
self.navigate("assets")
|
|
self.code("await page.getByRole('button',{name:'Menú global',exact:true}).click();return true;")
|
|
self.resize(768,1024)
|
|
self.code("await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
|
|
self.assertTrue(self.code("return await page.evaluate(()=>document.activeElement!==document.body&&document.activeElement.getClientRects().length>0);"))
|
|
self.resize(390,844)
|
|
self.assertEqual(self.code("return await page.getByRole('dialog').count();"),0)
|
|
self.tool('browser_navigate',{'url':self.origin+'/dashboard'})
|
|
self.code("await page.locator('main').waitFor();return true;")
|
|
self.assertEqual(self.code("return await page.locator('[data-slot=company-context-bar]').count();"),0)
|
|
self.code("await page.getByRole('button',{name:'Menú global',exact:true}).click();return true;")
|
|
self.assertEqual(self.code("return await page.getByRole('dialog').locator('a[href=\"/settings/users\"]').count();"),0)
|
|
self.capture("navigation-dashboard-menu-390")
|
|
self.code("await page.keyboard.press('Escape');return true;")
|
|
|
|
|
|
def test_mobile_navigation_long_names_and_text_scaling(self):
|
|
long_name="Compañía sintética de soporte e infraestructura con identificacióntécnicaextraordinariamentelarga"
|
|
self.resize(320,640)
|
|
self.code("""await page.route('**/api/companies{,/**}',async route=>{
|
|
const path=new URL(route.request().url()).pathname;
|
|
if(route.request().method()!=='GET'||!["/api/companies","/api"+"""+json.dumps(self.company)+"""].includes(path)){return route.fallback()}
|
|
const response=await route.fetch();const data=await response.json();
|
|
if(data.company){data.company.name="""+json.dumps(long_name)+"""}
|
|
if(data.companies){data.companies=data.companies.map(c=>({...c,name:"""+json.dumps(long_name)+"""}))}
|
|
await route.fulfill({response,json:data});
|
|
});return true;""")
|
|
try:
|
|
self.navigate('assets')
|
|
self.code("await page.getByRole('button',{name:"+json.dumps('Secciones de '+long_name)+",exact:true}).waitFor();return true;")
|
|
metrics=self.code("return await page.evaluate(()=>({mainY:document.querySelector('main').getBoundingClientRect().y,overflow:document.documentElement.scrollWidth>innerWidth}));")
|
|
self.record('navigation-long-name-320',metrics)
|
|
self.assertLessEqual(metrics['mainY'],128)
|
|
self.assertFalse(metrics['overflow'])
|
|
self.capture('navigation-long-name-320')
|
|
self.code("await page.getByRole('button',{name:"+json.dumps('Secciones de '+long_name)+",exact:true}).click();return true;")
|
|
self.resize(320,360)
|
|
self.code("await page.getByRole('dialog').locator('nav a').last().focus();return true;")
|
|
self.assertTrue(self.code("return await page.getByRole('dialog').getByRole('button',{name:'Cerrar navegación'}).evaluate(e=>{const r=e.getBoundingClientRect();return r.top>=0&&r.bottom<=innerHeight;});"))
|
|
self.assertTrue(self.code("return await page.evaluate(()=>document.activeElement.getBoundingClientRect().bottom<=innerHeight);"))
|
|
self.capture('navigation-short-window-320')
|
|
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
|
|
self.resize(390,844)
|
|
zoom=self.code("const before=await page.evaluate(()=>devicePixelRatio);await page.keyboard.press('Control+Equal');await page.keyboard.press('Control+Equal');const after=await page.evaluate(()=>devicePixelRatio);await page.keyboard.press('Control+Digit0');return {before,after,browserZoomSupported:after>before};")
|
|
self.record('navigation-browser-zoom-attempt',zoom)
|
|
self.code("await page.evaluate(()=>document.documentElement.style.fontSize='200%');return true;")
|
|
self.settle()
|
|
scaling=self.code("return await page.evaluate(()=>({fontSize:getComputedStyle(document.documentElement).fontSize,mainY:document.querySelector('main').getBoundingClientRect().y,overflow:document.documentElement.scrollWidth>innerWidth}));")
|
|
self.record('navigation-text-scale-200',scaling)
|
|
self.assertFalse(scaling['overflow'])
|
|
self.capture('navigation-text-scale-200')
|
|
finally:
|
|
self.code("await page.evaluate(()=>document.documentElement.style.fontSize='');await page.unroute('**/api/companies{,/**}');return true;")
|
|
|
|
|
|
def test_mobile_navigation_account_locale_and_edit(self):
|
|
self.resize(390,844)
|
|
self.navigate('assets')
|
|
self.code("await page.getByRole('button',{name:/^Secciones de /}).click();await page.getByRole('dialog').getByRole('button',{name:'Editar compañía',exact:true}).click();await page.getByRole('dialog',{name:'Editar compañía',exact:true}).waitFor();return true;")
|
|
self.capture('navigation-company-edit-390')
|
|
self.code("await page.getByRole('dialog',{name:'Editar compañía',exact:true}).getByRole('button',{name:'Cancelar',exact:true}).click();await page.getByRole('dialog').waitFor({state:'hidden'});await page.waitForFunction(()=>document.activeElement.getAttribute('aria-label')?.startsWith('Secciones de '));return true;")
|
|
self.code("await page.locator('summary[aria-label=\"Menú de usuario\"]').click();await page.getByRole('combobox',{name:'Idioma'}).click();await page.getByRole('option',{name:'English',exact:true}).click();await page.getByRole('button',{name:'Global menu',exact:true}).waitFor();await page.locator('summary').click();return true;")
|
|
self.assertTrue(self.code("return await page.locator('[data-slot=company-current-section]').innerText()==='Assets';"))
|
|
self.capture('navigation-english-390')
|
|
self.code("await page.getByRole('button',{name:/^Sections for /}).click();return true;")
|
|
self.assertEqual(self.code("return await page.getByRole('dialog').locator('nav a').count();"),21)
|
|
self.assertEqual(''.join(self.code("return await page.getByRole('dialog').locator('nav [aria-current=page]').innerText();").split()),'Assets1')
|
|
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});await page.getByRole('button',{name:'Global menu',exact:true}).click();return true;")
|
|
for label in ['Home','Companies','Catalogs']:
|
|
self.assertEqual(self.code("return await page.getByRole('dialog').getByRole('link',{name:"+json.dumps(label)+",exact:true}).count();"),1)
|
|
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});await page.locator('summary').click();await page.getByRole('combobox',{name:'Language'}).click();await page.getByRole('option',{name:'Español',exact:true}).click();await page.locator('summary').click();return true;")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|