273 lines
29 KiB
Python
273 lines
29 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Capturas comparables y recorridos MCP de M05/M07 sobre el preparador desechable."""
|
||
|
|
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 = [(1600, 1000), (1440, 900), (1280, 900), (1024, 768), (768, 1024), (390, 844), (320, 720)]
|
||
|
|
|
||
|
|
|
||
|
|
class WorkstationUiuxV22Tests(unittest.TestCase):
|
||
|
|
@classmethod
|
||
|
|
def setUpClass(cls):
|
||
|
|
value = os.environ.get("METIS_PLAYWRIGHT_STORAGE_STATE")
|
||
|
|
if not value:
|
||
|
|
raise unittest.SkipTest("Requiere visual-audit.py y las fixtures de puestos.")
|
||
|
|
state = (ROOT / 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 pertenecer al preparador.")
|
||
|
|
report = json.loads((state.parent / "report.json").read_text())
|
||
|
|
cls.fixture = json.loads((state.parent / ("workstation-v22-fixtures.json" if (state.parent / "workstation-v22-fixtures.json").exists() else "workstation-fixtures.json")).read_text())
|
||
|
|
cls.fixture_file=state.parent/"workstation-v22-fixtures.json"
|
||
|
|
cls.origin = report["origin"]
|
||
|
|
if urlparse(cls.origin).hostname != "localhost" or report.get("ordinaryUser") is not True or cls.fixture['origin'] != cls.origin or cls.fixture['companyId'] != report['companyId']:
|
||
|
|
raise RuntimeError("Requiere la instalación sintética desechable activa.")
|
||
|
|
cls.phase = os.environ.get("METIS_UIUX_PHASE", "despues")
|
||
|
|
if cls.phase not in ("antes", "despues"):
|
||
|
|
raise RuntimeError("Fase desconocida.")
|
||
|
|
cls.output = ROOT / "docs/auditorias/ficha-puesto-trabajo/implementacion-v2.2"
|
||
|
|
(cls.output / cls.phase).mkdir(parents=True, exist_ok=True)
|
||
|
|
cls.measurements = json.loads((cls.output / "mediciones.json").read_text()) if (cls.output / "mediciones.json").exists() else {}
|
||
|
|
cls.stack = contextlib.ExitStack()
|
||
|
|
cls.addClassCleanup(cls.stack.close)
|
||
|
|
config = tomllib.loads((ROOT / ".codex/config.toml").read_text())["mcp_servers"]["playwright"]
|
||
|
|
cls.client = cls.stack.enter_context(AUDIT["browser"](config, state))
|
||
|
|
cls.base = cls.origin + "/companies/" + cls.fixture['companyId']
|
||
|
|
cls.code("page.__uiuxErrors=[];page.on('pageerror',e=>page.__uiuxErrors.push(e.message));return true;")
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def tool(cls, name, args):
|
||
|
|
return AUDIT["tool"](cls.client, name, args)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def code(cls, body):
|
||
|
|
return AUDIT["result_json"](cls.tool("browser_run_code_unsafe", {"code": "async(page)=>{" + body + "}"}))
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def save(cls, key, value):
|
||
|
|
cls.measurements = json.loads((cls.output / "mediciones.json").read_text()) if (cls.output / "mediciones.json").exists() else {}
|
||
|
|
cls.measurements.setdefault(cls.phase, {})[key] = value
|
||
|
|
(cls.output / "mediciones.json").write_text(json.dumps(cls.measurements, ensure_ascii=False, indent=2) + "\n")
|
||
|
|
|
||
|
|
def navigate(self,key="main",complete=True):
|
||
|
|
self.tool('browser_navigate',{'url':self.base+'/workstations/'+self.fixture[key]})
|
||
|
|
self.code("await page.locator('main h1').waitFor();return true;")
|
||
|
|
if complete:
|
||
|
|
self.code("await page.locator('[data-pending-count]').waitFor();return true;")
|
||
|
|
if key=='main' and complete:
|
||
|
|
self.code("await page.getByRole('link',{name:'Ángela · administración',exact:true}).waitFor();await page.getByText('equipo-adm-01.example.invalid',{exact:false}).first().waitFor();return true;")
|
||
|
|
if key=='noProfiles' and complete:
|
||
|
|
self.code("await page.getByRole('region',{name:/^Personas y perfiles/}).getByText('Todavía no hay perfiles vinculados al ordenador principal.',{exact:true}).waitFor();await page.getByText('No hay instalaciones registradas.',{exact:true}).waitFor();await page.getByText('No hay métodos de asistencia registrados para este activo.',{exact:true}).waitFor();return true;")
|
||
|
|
self.code("await page.evaluate(async()=>{await document.fonts.ready;await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)))});return true;")
|
||
|
|
|
||
|
|
def api(self, path, data=None):
|
||
|
|
return self.code("const origin="+json.dumps(self.origin)+",path="+json.dumps(path)+",data="+json.dumps(data)+";const {csrfToken}=await(await page.request.get(origin+'/api/auth/session')).json();const r=data===null?await page.request.get(origin+'/api'+path):await page.request.post(origin+'/api'+path,{data,headers:{origin,'x-csrf-token':csrfToken}});if(!r.ok())throw Error('HTTP '+r.status());return r.json();")
|
||
|
|
|
||
|
|
def capture(self,name):
|
||
|
|
image=AUDIT['capture'](self.client,self.output/self.phase,name)
|
||
|
|
geometry=self.code("""return await page.evaluate(()=>{const rect=e=>{const r=e.getBoundingClientRect();return {x:r.x,y:r.y,width:r.width,height:r.height,right:r.right,bottom:r.bottom}};const titles=['Equipamiento','Personas y perfiles','Software','Soporte y acceso remoto','Último cambio','Configuraciones pendientes'];const panels=titles.map(title=>{const section=[...document.querySelectorAll('main section[aria-labelledby]')].find(e=>document.getElementById(e.getAttribute('aria-labelledby'))?.textContent.startsWith(title));if(!section)return {title,absent:true};const header=section.querySelector('[data-slot=card-header]');return {title,...rect(section),header:rect(header),text:section.innerText,tableCount:[...section.querySelectorAll('table')].filter(e=>e.getBoundingClientRect().width>0).length,actions:[...section.querySelectorAll('button')].filter(e=>e.getBoundingClientRect().width>0).map(e=>({name:e.getAttribute('aria-label')||e.textContent,disabled:e.disabled,...rect(e)}))}});return {viewport:innerWidth,scrollWidth:document.documentElement.scrollWidth,mainHeight:document.querySelector('main').getBoundingClientRect().height,panels};});""")
|
||
|
|
self.assertLessEqual(geometry['scrollWidth'],geometry['viewport'])
|
||
|
|
self.save(name,{'image':image,'geometry':geometry});print(self.phase+': '+name,flush=True)
|
||
|
|
|
||
|
|
def test_00_prepare(self):
|
||
|
|
if self.phase!='antes':self.skipTest('Las fixtures se preparan antes de modificar producción.')
|
||
|
|
previous=runpy.run_path(str(ROOT/'scripts/workstation-uiux-v21.test.py'))['WorkstationUiuxV21Tests']
|
||
|
|
previous.test_00_prepare_long_data_and_volume(self)
|
||
|
|
company=self.fixture['companyId'];inventory=self.api('/assets/'+company)
|
||
|
|
host=next(a for a in inventory['assets'] if a['id']==self.fixture['host']);monitor=next(a for a in inventory['assets'] if a['values']['name']=='MON-ADM-01 · Monitor panorámico')
|
||
|
|
for key,name,template,principal in [('noProfiles','UIUX-V22 · Equipo sin perfiles',host['values']['templateId'],True),('peripherals','UIUX-V22 · Periféricos sin ordenador',monitor['values']['templateId'],False)]:
|
||
|
|
asset=next((a for a in inventory['assets'] if a['values']['name']==name),None)
|
||
|
|
if not asset:asset=self.api('/assets/'+company,{'values':{'name':name,'templateId':template,'siteId':None,'status':'operational','notes':'Datos sintéticos de UI/UX v2.2.','characteristics':{}}})['asset']
|
||
|
|
station=next((w for w in inventory['workstations'] if w['values']['name']==name),None)
|
||
|
|
if not station:station=self.api('/assets/'+company+'/workstations',{'values':{'name':name,'active':True,'siteId':None,'personId':None,'computerAssetId':asset['id'] if principal else None,'assetIds':[asset['id']]}})['workstation']
|
||
|
|
self.fixture[key]=station['id']
|
||
|
|
self.fixture_file.write_text(json.dumps(self.fixture,ensure_ascii=False,indent=2)+'\n')
|
||
|
|
inventory=self.api('/assets/'+company)
|
||
|
|
self.save('volume',{'companies':15,'people':59,'assets':len(inventory['assets']),'resources':len(self.api('/resources/'+company)['resources']),'workstations':len(inventory['workstations']),'profiles':len(self.api('/work-profiles/'+company)['profiles'])})
|
||
|
|
|
||
|
|
def test_01_comparable_captures(self):
|
||
|
|
for width,height in SIZES:
|
||
|
|
self.tool('browser_resize',{'width':width,'height':height});self.navigate();self.capture('principal-'+str(width))
|
||
|
|
self.navigate('empty');self.capture('sin-principal-'+str(width))
|
||
|
|
for width,height in [(1440,900),(390,844),(320,720)]:
|
||
|
|
self.tool('browser_resize',{'width':width,'height':height});self.navigate()
|
||
|
|
self.code("await page.getByRole('region',{name:/^Software/}).scrollIntoViewIfNeeded();return true;");self.capture('software-'+str(width))
|
||
|
|
self.code("await page.getByRole('region',{name:'Último cambio',exact:true}).scrollIntoViewIfNeeded();return true;");self.capture('ultimo-cambio-'+str(width))
|
||
|
|
for key,label in [('noProfiles','vacios-confirmados'),('retired','desmantelado'),('peripherals','sin-principal-con-perifericos')]:
|
||
|
|
for width,height in [(1440,900),(390,844)]:
|
||
|
|
self.tool('browser_resize',{'width':width,'height':height});self.navigate(key);self.capture(label+'-'+str(width))
|
||
|
|
self.save('pageErrors',self.code('return page.__uiuxErrors;'));self.assertEqual(self.code('return page.__uiuxErrors;'),[])
|
||
|
|
|
||
|
|
def test_02_comparable_errors_and_loading(self):
|
||
|
|
route='**/api/software-installations/'+self.fixture['companyId']
|
||
|
|
self.code('await page.route('+json.dumps(route)+",r=>r.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;")
|
||
|
|
try:
|
||
|
|
for width,height in [(1440,900),(390,844)]:
|
||
|
|
self.tool('browser_resize',{'width':width,'height':height});self.navigate()
|
||
|
|
self.code("await page.getByRole('region',{name:/^Software/}).getByRole('alert').waitFor();return true;");self.capture('error-software-'+str(width))
|
||
|
|
finally:self.code('await page.unroute('+json.dumps(route)+');return true;')
|
||
|
|
self.code('await page.route('+json.dumps(route)+",r=>{page.__heldSoftware=r});return true;")
|
||
|
|
try:
|
||
|
|
self.tool('browser_resize',{'width':1440,'height':900});self.navigate()
|
||
|
|
self.code("await page.getByRole('region',{name:/^Software/}).getByRole('status').waitFor();return true;");self.capture('carga-software-1440')
|
||
|
|
finally:self.code('if(page.__heldSoftware){await page.__heldSoftware.continue();delete page.__heldSoftware}await page.unroute('+json.dumps(route)+');return true;')
|
||
|
|
|
||
|
|
def expand_primary(self):
|
||
|
|
self.code("await page.getByRole('button',{name:'Mostrar configuraciones de Ángela · administración',exact:true}).click();await page.locator('#workstation-configurations').waitFor();return true;")
|
||
|
|
|
||
|
|
def open_management(self):
|
||
|
|
self.code("await page.getByRole('button',{name:'Gestionar soporte',exact:true}).click();await page.getByRole('dialog',{name:'Gestionar soporte',exact:true}).waitFor();return true;")
|
||
|
|
|
||
|
|
def test_03_action_scope_keyboard_and_history(self):
|
||
|
|
results=[]
|
||
|
|
for width,height in [(1440,900),(390,844),(320,720)]:
|
||
|
|
self.tool('browser_resize',{'width':width,'height':height});self.navigate()
|
||
|
|
for source in ['header','equipment']:
|
||
|
|
value=self.code("const trigger="+("page.locator('main header').first()" if source=='header' else "page.getByRole('region',{name:/^Equipamiento/})")+".getByRole('button',{name:'Editar puesto',exact:true});await trigger.focus();await trigger.press('Enter');const dialog=page.getByRole('dialog',{name:'Editar puesto',exact:true});await dialog.waitFor();const labels=await dialog.locator('label').allTextContents();const name=await dialog.getByRole('textbox',{name:'Nombre',exact:true}).inputValue();await dialog.getByRole('button',{name:'Cancelar',exact:true}).click();await dialog.waitFor({state:'hidden'});return {labels,name,focus:await trigger.evaluate(e=>e===document.activeElement)};")
|
||
|
|
self.assertTrue(value['focus']);self.assertIn('Administración',value['name']);self.assertTrue(any('Sede' in x for x in value['labels']));self.assertTrue(any('Responsable' in x for x in value['labels']));results.append({'width':width,'source':source,**value})
|
||
|
|
histories=[]
|
||
|
|
for label in ['Ver historial','Ver historial completo']:
|
||
|
|
value=self.code("const trigger=page.getByRole('button',{name:"+json.dumps(label)+",exact:true});await trigger.focus();await trigger.press('Enter');const dialog=page.getByRole('dialog').last();await dialog.getByText('Administración — puesto inicial',{exact:true}).waitFor();const text=await dialog.innerText();await page.keyboard.press('Escape');await dialog.waitFor({state:'hidden'});return {text,focus:await trigger.evaluate(e=>e===document.activeElement)};")
|
||
|
|
self.assertTrue(value['focus']);histories.append(value['text'])
|
||
|
|
self.assertEqual(histories[0],histories[1])
|
||
|
|
value=self.code("const p=page.getByRole('region',{name:/^Software/}),trigger=p.getByRole('button',{name:'Gestionar software',exact:true});const card=await p.boundingBox(),button=await trigger.boundingBox();await trigger.focus();await trigger.press('Enter');const dialog=page.getByRole('dialog').last();await dialog.getByRole('button',{name:'Añadir instalación',exact:true}).waitFor();await page.keyboard.press('Escape');await dialog.waitFor({state:'hidden'});return {card,button,focus:await trigger.evaluate(e=>e===document.activeElement)};")
|
||
|
|
self.assertTrue(value['focus']);self.assertLessEqual(value['button']['x']+value['button']['width'],value['card']['x']+value['card']['width']);results.append({'width':width,'software':value})
|
||
|
|
self.navigate('empty')
|
||
|
|
self.code("const b=page.getByRole('region',{name:/^Equipamiento/}).getByRole('button',{name:'Editar puesto',exact:true});await b.focus();await b.press('Space');const d=page.getByRole('dialog',{name:'Editar puesto',exact:true});await d.waitFor();await page.keyboard.press('Escape');await d.waitFor({state:'hidden'});if(!await b.evaluate(e=>e===document.activeElement))throw Error('Foco no restituido');return true;")
|
||
|
|
self.save('actionScopes',results)
|
||
|
|
|
||
|
|
def test_04_dependency_and_source_states(self):
|
||
|
|
self.tool('browser_resize',{'width':390,'height':844})
|
||
|
|
for key in ['empty','peripherals','retired','noProfiles']:
|
||
|
|
self.navigate(key)
|
||
|
|
value=self.code("const p=page.getByRole('region',{name:/^Personas y perfiles/});return {key:"+json.dumps(key)+",text:await page.locator('main').innerText(),profileTableVisible:await p.locator('table').isVisible(),disabled:await p.getByRole('button',{name:'Nuevo perfil',exact:true}).isDisabled()};")
|
||
|
|
self.assertFalse(value['profileTableVisible']);self.assertEqual(value['disabled'],key!='noProfiles')
|
||
|
|
if key=='retired':self.assertIn('El puesto está inactivo',value['text']);self.assertNotIn('Asigna un ordenador principal',value['text'])
|
||
|
|
if key!='noProfiles':self.assertNotIn('Todavía no hay perfiles vinculados',value['text'])
|
||
|
|
self.save('state-'+key,value)
|
||
|
|
route='**/api/work-profiles/'+self.fixture['companyId']
|
||
|
|
self.code('await page.route('+json.dumps(route)+",r=>{page.__heldProfiles=r});return true;")
|
||
|
|
try:
|
||
|
|
self.navigate(complete=False)
|
||
|
|
self.code("await page.getByRole('region',{name:'Contexto remoto',exact:true}).getByRole('status').waitFor();return true;")
|
||
|
|
self.assertFalse(self.code("return await page.getByRole('region',{name:'Contexto remoto',exact:true}).getByText('No hay perfiles activos para este ordenador.',{exact:true}).isVisible();"))
|
||
|
|
finally:self.code('if(page.__heldProfiles){await page.__heldProfiles.continue();delete page.__heldProfiles}await page.unroute('+json.dumps(route)+');return true;')
|
||
|
|
self.code('await page.route('+json.dumps(route)+",r=>r.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;")
|
||
|
|
try:
|
||
|
|
self.navigate(complete=False)
|
||
|
|
self.code("await page.getByRole('region',{name:'Contexto remoto',exact:true}).getByRole('alert').waitFor();return true;")
|
||
|
|
self.assertFalse(self.code("return await page.getByRole('region',{name:'Contexto remoto',exact:true}).getByText('No hay perfiles activos para este ordenador.',{exact:true}).isVisible();"))
|
||
|
|
self.assertEqual(self.code("return await page.getByRole('region',{name:/^Personas y perfiles/}).locator('h2 span.tabular-nums').count();"),0)
|
||
|
|
finally:self.code('await page.unroute('+json.dumps(route)+');return true;')
|
||
|
|
self.code("const p=page.getByRole('region',{name:/^Personas y perfiles/});await p.getByRole('button',{name:'Reintentar',exact:true}).click();await p.getByRole('link',{name:'Ángela · administración',exact:true}).waitFor();return true;")
|
||
|
|
self.save('profileSourceStates',{'loadingNotEmpty':True,'errorNotEmpty':True,'retryRecovered':True})
|
||
|
|
|
||
|
|
def test_05_unknown_principal_and_lifecycle(self):
|
||
|
|
route='**/api/assets/'+self.fixture['companyId'];host=self.fixture['host']
|
||
|
|
for state in ['missing-reference','retired','inactive']:
|
||
|
|
code="json.assets=json.assets.filter(a=>a.id!=="+json.dumps(host)+");" if state=='missing-reference' else "for(const a of json.assets){if(a.id==="+json.dumps(host)+")a.values.status='retired'}" if state=='retired' else "for(const w of json.workstations){if(w.id==="+json.dumps(self.fixture['main'])+")w.values.active=false}"
|
||
|
|
self.code('await page.route('+json.dumps(route)+",async route=>{const r=await route.fetch();const json=await r.json();"+code+"await route.fulfill({response:r,json})});return true;")
|
||
|
|
try:
|
||
|
|
self.navigate(complete=state!='missing-reference')
|
||
|
|
if state=='missing-reference':
|
||
|
|
self.code("await page.getByText('No se puede confirmar el total de pendientes.',{exact:true}).waitFor();return true;")
|
||
|
|
self.assertEqual(self.code("return await page.getByRole('region',{name:/^Personas y perfiles/}).locator('h2 span.tabular-nums').count();"),0)
|
||
|
|
self.assertFalse(self.code("return await page.getByText('Todavía no hay perfiles vinculados al ordenador principal.',{exact:true}).isVisible();"))
|
||
|
|
else:
|
||
|
|
self.assertTrue(self.code("return await page.getByRole('button',{name:'Nuevo perfil',exact:true}).isDisabled();"))
|
||
|
|
self.code("await page.getByRole('button',{name:'Gestionar software',exact:true}).click();await page.getByRole('dialog').last().getByRole('button',{name:'Añadir instalación',exact:true}).waitFor();return true;")
|
||
|
|
self.assertTrue(self.code("return await page.getByRole('dialog').last().getByRole('button',{name:'Añadir instalación',exact:true}).isDisabled();"))
|
||
|
|
text=self.code("return await page.getByRole('dialog').last().innerText();")
|
||
|
|
self.assertIn('no se pueden añadir instalaciones',text)
|
||
|
|
self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
|
||
|
|
self.save('lifecycle-'+state,{'passed':True})
|
||
|
|
finally:self.code('await page.unroute('+json.dumps(route)+');return true;')
|
||
|
|
|
||
|
|
def test_06_regressions(self):
|
||
|
|
selected=set(filter(None,os.environ.get('METIS_UIUX_REGRESSION','').split(',')))
|
||
|
|
namespace=runpy.run_path(str(ROOT/'scripts/workstation-uiux-v21.test.py'));case=namespace['WorkstationUiuxV21Tests'];v21_names=set(unittest.defaultTestLoader.getTestCaseNames(case));passed=list(self.measurements.get(self.phase,{}).get('v21Regressions',[]))
|
||
|
|
for name in ['test_02_container_thresholds','test_03_dense_scope_and_keyboard','test_04_technical_disclosure_and_pending','test_09_totals_change_during_refetch','test_10_profile_space_full_values_and_refetch','test_11_entity_family_navigation_and_isolation']:
|
||
|
|
if selected and name not in selected:continue
|
||
|
|
print('Regresión v2.1: '+name,flush=True);getattr(case,name)(self)
|
||
|
|
if name not in passed:passed.append(name)
|
||
|
|
self.save('v21Regressions',passed)
|
||
|
|
self.save('v21Regressions',passed)
|
||
|
|
source=ROOT/'scripts/workstation-uiux-v1.test.py';code=source.read_text().replace("name:'Equipamiento',exact:true","name:/^Equipamiento/");namespace={'__file__':str(source),'__name__':'v1_regressions_v22'};exec(compile(code,str(source),'exec'),namespace);case=namespace['WorkstationUiuxV1Tests'];passed=list(self.measurements.get(self.phase,{}).get('v1Regressions',[]))
|
||
|
|
if selected and selected-set(unittest.defaultTestLoader.getTestCaseNames(case))-v21_names:
|
||
|
|
raise ValueError('Filtro de regresión desconocido')
|
||
|
|
for name in unittest.defaultTestLoader.getTestCaseNames(case):
|
||
|
|
if not name.startswith('test_01') and (not selected or name in selected):
|
||
|
|
print('Regresión v1: '+name,flush=True);getattr(case,name)(self)
|
||
|
|
if name not in passed:passed.append(name)
|
||
|
|
self.save('v1Regressions',passed)
|
||
|
|
self.save('v1Regressions',passed)
|
||
|
|
|
||
|
|
def test_07_save_existing_editor(self):
|
||
|
|
company=self.fixture['companyId'];original=next(w for w in self.api('/assets/'+company)['workstations'] if w['id']==self.fixture['main']);name='UIUX-V22 · Edición comprobada'
|
||
|
|
self.navigate()
|
||
|
|
try:
|
||
|
|
self.code("await page.getByRole('region',{name:/^Equipamiento/}).getByRole('button',{name:'Editar puesto',exact:true}).click();const d=page.getByRole('dialog',{name:'Editar puesto',exact:true});await d.getByRole('textbox',{name:'Nombre',exact:true}).fill("+json.dumps(name)+");await d.getByRole('button',{name:'Guardar',exact:true}).click();await d.waitFor({state:'hidden'});await page.getByRole('heading',{level:1,name:"+json.dumps(name)+",exact:true}).waitFor();return true;")
|
||
|
|
saved=next(w for w in self.api('/assets/'+company)['workstations'] if w['id']==original['id']);self.assertEqual(saved['values']['name'],name);self.assertEqual(saved['values']['assetIds'],original['values']['assetIds'])
|
||
|
|
self.save('editorSave',{'saved':True,'compositionPreserved':True,'historyAdded':len(saved['history'])>len(original['history'])})
|
||
|
|
finally:self.api('/assets/'+company+'/workstations/'+original['id']+'/update',{'values':original['values']})
|
||
|
|
|
||
|
|
def test_08_inventory_uncertainty_and_recovery(self):
|
||
|
|
self.tool('browser_resize',{'width':390,'height':844});self.navigate('empty')
|
||
|
|
route='**/api/assets/'+self.fixture['companyId']
|
||
|
|
self.code('await page.route('+json.dumps(route)+",r=>r.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));await page.context().setOffline(true);await page.context().setOffline(false);return true;")
|
||
|
|
try:
|
||
|
|
self.code("await page.getByRole('region',{name:/^Equipamiento/}).getByRole('alert').waitFor();return true;")
|
||
|
|
value=self.code("return {equipmentCount:await page.getByRole('region',{name:/^Equipamiento/}).locator('h2 span.tabular-nums').count(),profilesCount:await page.getByRole('region',{name:/^Personas y perfiles/}).locator('h2 span.tabular-nums').count(),pending:await page.locator('[data-pending-count]').count(),text:await page.locator('main').innerText()};")
|
||
|
|
self.assertEqual((value['equipmentCount'],value['profilesCount'],value['pending']),(0,0,0));self.assertNotIn('Este puesto todavía no tiene activos.',value['text']);self.assertIn('No se puede confirmar el total',value['text']);self.capture('inventario-incierto-390')
|
||
|
|
finally:self.code('await page.unroute('+json.dumps(route)+');return true;')
|
||
|
|
self.code("await page.getByRole('region',{name:/^Personas y perfiles/}).getByRole('button',{name:'Reintentar',exact:true}).click();await page.getByRole('region',{name:/^Equipamiento/}).getByText('Este puesto todavía no tiene activos.',{exact:true}).waitFor();return true;")
|
||
|
|
self.assertFalse(self.code("return await page.evaluate(()=>document.activeElement===document.body);"))
|
||
|
|
self.assertEqual(self.code("return await page.getByRole('region',{name:/^Equipamiento/}).locator('h2 span.tabular-nums').innerText();"),'0');self.save('inventoryUncertainty',{'error':value,'retryRestoresKnownZero':True})
|
||
|
|
for width,height in [(1440,900),(390,844)]:
|
||
|
|
self.tool('browser_resize',{'width':width,'height':height});self.navigate('retired');self.capture('desmantelado-'+str(width))
|
||
|
|
for width,height in [(390,844),(320,720)]:
|
||
|
|
self.tool('browser_resize',{'width':width,'height':height});self.navigate('empty');self.code("await page.getByRole('region',{name:'Soporte y acceso remoto',exact:true}).scrollIntoViewIfNeeded();return true;");self.capture('dependencias-'+str(width))
|
||
|
|
|
||
|
|
def test_09_complete_empty_equipment(self):
|
||
|
|
company=self.fixture['companyId'];inventory=self.api('/assets/'+company);original=next(w for w in inventory['workstations'] if w['id']==self.fixture['empty']);asset=next(a for a in inventory['assets'] if a['values']['name']=='AUDIT-PORT-01')
|
||
|
|
self.tool('browser_resize',{'width':390,'height':844});self.navigate('empty')
|
||
|
|
try:
|
||
|
|
self.code("await page.getByRole('region',{name:/^Equipamiento/}).getByRole('button',{name:'Editar puesto',exact:true}).click();const d=page.getByRole('dialog',{name:'Editar puesto',exact:true});await d.getByRole('button',{name:'Seleccionar activos',exact:true}).click();const picker=page.getByRole('dialog',{name:'Seleccionar activos',exact:true});await picker.locator('input[data-quick-search]').fill('AUDIT-PORT-01');await picker.getByRole('checkbox',{name:'Seleccionar AUDIT-PORT-01',exact:true}).check();return true;")
|
||
|
|
print('Vacío: activo seleccionado en picker',flush=True)
|
||
|
|
options=self.code("const d=page.getByRole('dialog',{name:'Editar puesto',exact:true}),picker=page.getByRole('dialog',{name:'Seleccionar activos',exact:true});await picker.getByRole('button',{name:'Seleccionar',exact:true}).click();await picker.waitFor({state:'hidden'});return await d.getByRole('radio').evaluateAll(es=>es.map(e=>({label:e.getAttribute('aria-label'),disabled:e.disabled})));")
|
||
|
|
print('Vacío: opciones de principal',options,flush=True)
|
||
|
|
self.code("const d=page.getByRole('dialog',{name:'Editar puesto',exact:true});const options=await d.getByRole('radio').evaluateAll(es=>es.map(e=>({label:e.getAttribute('aria-label'),disabled:e.disabled})));return options;")
|
||
|
|
self.code("const group=page.getByRole('dialog',{name:'Editar puesto',exact:true}).getByRole('radiogroup');const radios=group.getByRole('radio');if(await radios.count()!==1)throw Error('Se esperaba un único activo elegido');await radios.first().focus();await radios.first().press('Space');return {checked:await radios.first().getAttribute('aria-checked')};")
|
||
|
|
print('Vacío: ordenador principal seleccionado',flush=True)
|
||
|
|
self.code("const d=page.getByRole('dialog',{name:'Editar puesto',exact:true});await d.getByRole('button',{name:'Guardar',exact:true}).click();await d.waitFor({state:'hidden'});await page.getByRole('region',{name:/^Equipamiento/}).getByRole('link',{name:'AUDIT-PORT-01',exact:true}).waitFor();return true;")
|
||
|
|
value=self.code("return {body:await page.evaluate(()=>document.activeElement===document.body),focus:await page.evaluate(()=>document.activeElement?.textContent.trim()),profilesEnabled:!await page.getByRole('button',{name:'Nuevo perfil',exact:true}).isDisabled()};")
|
||
|
|
self.save('emptyCompletion',value);self.assertFalse(value['body']);self.assertEqual(value['focus'],'Editar puesto');self.assertTrue(value['profilesEnabled'])
|
||
|
|
finally:self.api('/assets/'+company+'/workstations/'+original['id']+'/update',{'values':original['values']})
|
||
|
|
|
||
|
|
def test_10_native_zoom_probe(self):
|
||
|
|
self.tool('browser_resize',{'width':1440,'height':900});self.navigate()
|
||
|
|
before=self.code("await page.keyboard.press('Control+Digit0');return await page.evaluate(()=>({width:innerWidth,dpr:devicePixelRatio}));")
|
||
|
|
self.code("await page.keyboard.press('Control+Equal');await page.evaluate(()=>new Promise(requestAnimationFrame));return true;")
|
||
|
|
first=self.code("return await page.evaluate(()=>({width:innerWidth,dpr:devicePixelRatio}));")
|
||
|
|
try:
|
||
|
|
if first==before:
|
||
|
|
self.save('nativeZoom',{'available':False,'before':before,'afterShortcut':first,'reason':'El atajo nativo no cambia el zoom en el Chromium headless de esta sesión.'})
|
||
|
|
return
|
||
|
|
for target in [2,4]:
|
||
|
|
self.code("for(let i=0;i<12&&await page.evaluate(()=>devicePixelRatio)<"+str(target*before['dpr'])+";i++){await page.keyboard.press('Control+Equal');await page.evaluate(()=>new Promise(requestAnimationFrame))}return true;")
|
||
|
|
value=self.code("return await page.evaluate(()=>({width:innerWidth,dpr:devicePixelRatio,scrollWidth:document.documentElement.scrollWidth}));")
|
||
|
|
self.assertAlmostEqual(value['dpr']/before['dpr'],target,delta=.01);self.assertLessEqual(value['scrollWidth'],value['width']);self.capture('zoom-nativo-'+str(target*100));self.save('nativeZoom-'+str(target*100),value)
|
||
|
|
self.save('nativeZoom',{'available':True,'tested':[200,400]})
|
||
|
|
finally:self.code("await page.keyboard.press('Control+Digit0');return true;")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__=='__main__':
|
||
|
|
unittest.main()
|