metis-desk/scripts/adoption-ui.test.py

332 lines
35 KiB
Python
Raw Normal View History

2026-10-24 23:19:17 +00:00
#!/usr/bin/env python3
"""L04.1 mediante el MCP existente: URL, retorno y datos sintéticos de lectura.
Reutiliza visual-audit.py y list-context-ui.test.py. No inicia servicios ni guarda
entidades; amplía el grafo mediante GET y valida los payloads con los schemas reales.
"""
import json
import os
from pathlib import Path
import runpy
import re
import subprocess
import unittest
from urllib.parse import parse_qs, urlsplit, urlencode
ROOT = Path(__file__).resolve().parent.parent
CONTEXT = runpy.run_path(str(ROOT / "scripts/list-context-ui.test.py"))
class AdoptionUiTests(CONTEXT["ListContextUiTests"]):
@classmethod
def code(cls, body):
try:
return CONTEXT['AUDIT']['result_json'](cls.client.tool('browser_run_code_unsafe', {'code': 'async(page)=>{' + body + '}'}))
except RuntimeError as error:
# Playwright incluye cabeceras en algunos timeouts de request; nunca conservar cookies.
message = re.sub(r'(?im)(cookie:|metis\.sid=)[^\r\n]*', r'\1 [redactado]', str(error))
with (cls.output / ('tool-errors-' + str(os.getpid()) + '.txt')).open('a') as diagnostic:
diagnostic.write(message+'\n')
try:
detail = CONTEXT['AUDIT']['result_json'](cls.client.tool('browser_run_code_unsafe', {'code': "async(page)=>({page:await page.evaluate(()=>({url:location.pathname+location.search,main:document.querySelector('main')?.innerText,body:document.body.innerText.slice(0,2000)})),console:page.adoptionConsole,responses:page.adoptionResponses})"}))
(cls.output / ('failure-context-' + str(os.getpid()) + '.json')).write_text(json.dumps(detail,ensure_ascii=False,indent=2)+'\n')
except RuntimeError:
# Se conserva el fallo original aunque el documento deje de estar disponible.
pass
raise RuntimeError(message) from None
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.output = getattr(cls, "artifact_directory", ROOT / ".artifacts/playwright/l04-1/after")
cls.output.mkdir(parents=True, exist_ok=True)
cls.measurements = cls.output / ("measurements-" + str(os.getpid()) + ".json")
cls.code('page.adoptionOrigin=' + json.dumps(cls.origin) + ';page.adoptionCompany=' + json.dumps(cls.company) + ';return true;')
cls.code("""await page.routeWebSocket(url=>url.host===new URL(page.adoptionOrigin).host,()=>{});
await page.route(page.adoptionOrigin+'/**',async route=>{if(route.request().method()!=='GET')return route.fallback();try{const response=await route.fetch({maxRetries:2});await route.fulfill({response})}catch(error){page.adoptionConsole?.push({type:'transport-error',url:route.request().url(),text:error.name});await route.abort('failed')}});page.adoptionConsole=[];page.adoptionGoto=async url=>{try{return await page.goto(url,{waitUntil:'domcontentloaded'})}catch(e){if(!String(e).includes('ERR_NETWORK_CHANGED'))throw e;page.adoptionConsole.push({type:'infrastructure-retry',url,text:'ERR_NETWORK_CHANGED: un segundo intento de navegación'});return page.goto(url,{waitUntil:'domcontentloaded'})}};page.on('console',m=>{if(['error','warning'].includes(m.type()))page.adoptionConsole.push({type:m.type(),url:page.url(),text:m.text()})});page.on('pageerror',e=>page.adoptionConsole.push({type:'pageerror',url:page.url(),text:e.message}));
const get=async p=>{const r=await page.request.get(page.adoptionOrigin+'/api'+p);if(r.status()!==200)throw Error('GET sintético '+p+': '+r.status());return r.json()};
const detail=await get('/companies/'+page.adoptionCompany),inventory=await get('/assets/'+page.adoptionCompany),companies=await get('/companies'),resources=await get('/resources/'+page.adoptionCompany),session=await get('/auth/session');
if(companies.companies.length<15||detail.company.people.length<50||inventory.assets.length<50||resources.resources.length<50)throw Error('Preparar volumen con visual-audit-volume.py');
const baseline={detail:structuredClone(detail),inventory:structuredClone(inventory),companies:structuredClone(companies),profiles:await get('/work-profiles/'+page.adoptionCompany)};const company=detail.company,id=n=>'019a4000-0000-7000-a000-'+String(n).padStart(12,'0');
company.sites=Array.from({length:50},(_,i)=>({id:id(1000+i),companyId:company.id,name:'L04-SEDE-'+String(i+1).padStart(2,'0')+' · Administración Álvarez',active:true,address:'Dirección de prueba '+(i+1),phone:null}));
const people=company.people.filter(p=>p.active),computers=inventory.assets.filter(a=>a.values.status==='operational'&&a.specification.templateName==='Portátil');
if(!computers.length||!people.length)throw Error('Faltan referencias válidas');
while(computers.length<50){const n=computers.length,a=structuredClone(computers[0]);a.id=id(2000+n);a.values.name='L04-COMPUTER-'+String(n+1).padStart(2,'0');a.history=[{id:id(3000+n),values:a.values,actorId:session.user.id,actorName:session.user.name,recordedAt:'2026-03-01T09:00:00.000Z'}];inventory.assets.push(a);computers.push(a)}
const peripherals=inventory.assets.filter(a=>a.specification.templateName==='Monitor');
inventory.workstations=Array.from({length:50},(_,i)=>{const active=(i+1)%5!==0,computer=computers[i],name='L04-PUESTO-'+String(i+1).padStart(2,'0')+' · Administración '+(i===1?'Pena':'Peña');const initial={name,active:true,siteId:company.sites[i].id,personId:people[i%people.length].id,computerAssetId:computer.id,assetIds:[computer.id,...(i<peripherals.length?[peripherals[i].id]:[])]};const values=active?initial:{...initial,active:false,personId:null,computerAssetId:null,assetIds:[]};computer.assigned=active;computer.displayStatus=active?'in_use':'available';const history=[{id:id(5000+i),values:initial,actorId:session.user.id,actorName:session.user.name,recordedAt:'2026-03-01T09:00:00.000Z'}];if(!active)history.push({id:id(6000+i),values,actorId:session.user.id,actorName:session.user.name,recordedAt:'2026-03-02T09:00:00.000Z'});return {id:id(4000+i),companyId:company.id,values,history}});
const occupied=new Set(inventory.workstations.filter(w=>w.values.active).flatMap(w=>w.values.assetIds));for(const a of inventory.assets){a.assigned=occupied.has(a.id);a.displayStatus=a.assigned?'in_use':a.values.status==='operational'?'available':a.displayStatus};const profiles={profiles:Array.from({length:50},(_,i)=>{const values={name:'L04-PERFIL-'+String(i+1).padStart(2,'0')+' · Administración '+(i===1?'Pena':'Peña'),personId:people[i%people.length].id,assetId:computers[i].id,accountId:null,active:(i+1)%5!==0,notes:null};const initial={...values,active:true};const history=[{id:id(8000+i),values:initial,actorId:session.user.id,actorName:session.user.name,recordedAt:'2026-03-01T09:00:00.000Z'}];if(!values.active)history.push({id:id(9000+i),values,actorId:session.user.id,actorName:session.user.name,recordedAt:'2026-03-02T09:00:00.000Z'});return {id:id(7000+i),companyId:company.id,values,needsReview:false,history}})};
companies.companies=companies.companies.map((c,i)=>({...c,name:c.id!==company.id&&i%3===0?c.name+' · Diseño e ingeniería Álvarez Peña / Delegación central':c.name,active:c.id===company.id||i%5!==0}));
const snapshots={};for(const path of ['/resources/','/resource-catalog/','/asset-catalog/','/access/','/mail-configurations/','/folder-mappings/','/printer-configurations/','/application-configurations/','/certificate-configurations/','/remote-connections/','/telework-authorizations/','/asset-infrastructure/','/software-installations/']){snapshots[path+company.id]=await get(path+company.id)};const support=await get('/asset-support/'+company.id+'/'+computers[0].id);page.adoption={detail,inventory,companies,profiles,snapshots,support,baseline};page.adoptionRemoved=null;
const routes=[['/companies',()=>page.adoption.companies],['/companies/'+company.id,()=>page.adoption.detail],['/assets/'+company.id,()=>({...page.adoption.inventory,workstations:page.adoption.inventory.workstations.filter(w=>w.id!==page.adoptionRemoved)})],['/work-profiles/'+company.id,()=>({...page.adoption.profiles,profiles:page.adoption.profiles.profiles.filter(p=>p.id!==page.adoptionRemoved)})]];
await page.route(page.adoptionOrigin+'/api/asset-support/'+company.id+'/*',r=>r.request().method()==='GET'?r.fulfill({status:200,contentType:'application/json',json:page.adoption.support}):r.fallback());for(const [path,data]of Object.entries(snapshots))routes.push([path,()=>data]);for(const [path,payload]of routes)await page.route(page.adoptionOrigin+'/api'+path,r=>r.request().method()==='GET'?r.fulfill({status:200,contentType:'application/json',json:payload()}):r.fallback());
await page.route(page.adoptionOrigin+'/api/companies/*',async r=>{const companyId=new URL(r.request().url()).pathname.split('/').at(-1),summary=page.adoption.companies.companies.find(c=>c.id===companyId);if(r.request().method()!=='GET'||companyId===company.id||!summary)return r.fallback();const response=await r.fetch();if(!response.ok())return r.fulfill({response});const data=await response.json();await r.fulfill({response,json:{...data,company:{...data.company,name:summary.name,active:summary.active}}})});
return {companies:companies.companies.length,people:company.people.length,assets:inventory.assets.length,resources:resources.resources.length,workstations:inventory.workstations.length,profiles:profiles.profiles.length};""")
fixture_path = cls.output / ("fixtures-" + str(os.getpid()) + ".json")
fixture_path.write_text(json.dumps(cls.code('return page.adoption;'), ensure_ascii=False) + '\n')
validation = """import {readFile} from 'node:fs/promises';
import {InventoryResponseSchema} from './modules/assets/src/common/inventory.ts';
import {CompaniesResponseSchema,CompanyResponseSchema} from './modules/companies/src/common/dto/response/company.response.dto.ts';
import {WorkProfilesResponseSchema} from './modules/companies/src/common/work-profiles.ts';
const f=JSON.parse(await readFile(process.argv[1],'utf8'));InventoryResponseSchema.parse(f.inventory);CompaniesResponseSchema.parse(f.companies);CompanyResponseSchema.parse(f.detail);WorkProfilesResponseSchema.parse(f.profiles);console.log('Schemas reales: correctos.');"""
result = subprocess.run(['node', '--import', 'tsx', '--input-type=module', '-e', validation, str(fixture_path)], cwd=ROOT, capture_output=True, text=True, timeout=60)
(cls.output / ('fixture-validation-' + str(os.getpid()) + '.log')).write_text(result.stdout + result.stderr)
if result.returncode:
raise RuntimeError('Fixtures incompatibles con los schemas; consultar evidencia.')
@classmethod
def tearDownClass(cls):
(cls.output / ('console-all-' + str(os.getpid()) + '.json')).write_text(json.dumps(cls.code('return page.adoptionConsole;'),ensure_ascii=False,indent=2)+'\n')
super().tearDownClass()
def navigate(self, section='assets', params=None):
suffix='?'+urlencode(params) if params else ''
self.code('await page.adoptionGoto('+json.dumps(self.origin+self.base+'/'+section+suffix)+');return true;')
self.ready(section)
def list(self, section, params=None):
self.navigate(section, params)
self.code("await page.locator('main [data-slot=entity-table][data-query-state=ready]').waitFor();return true;")
self.settle()
def directory(self, params=None):
url=self.origin+'/companies'+('?' + urlencode(params) if params else '')
self.code('await page.adoptionGoto('+json.dumps(url)+");await page.locator('main [data-slot=entity-table][data-query-state=ready]').waitFor();return true;")
self.settle()
def values(self):
return self.code(r"""return page.locator('main [data-slot=entity-table]').first().evaluate(root=>{
const table=root.querySelector('table'),v=root.closest('[data-slot=workspace-scroll]'),x=root.querySelector('[data-slot=table-container]'),input=root.querySelector('input[data-quick-search]'),a=document.activeElement;
return {url:location.pathname+location.search,query:input.value,page:root.querySelector('[aria-current=page]')?.textContent,sort:table.querySelector('th[aria-sort]')?.getAttribute('aria-sort'),count:Number(root.textContent.match(/(?:de|of) (\d+) /)?.[1]??0),rows:[...table.querySelectorAll('tbody tr[data-entity-id]')].map(r=>({id:r.dataset.entityId,name:r.querySelector('a')?.textContent})),x:x.scrollLeft,y:v.scrollTop,maxX:x.scrollWidth-x.clientWidth,maxY:v.scrollHeight-v.clientHeight,focus:a.tagName,focusRow:a.closest('[data-entity-id]')?.dataset.entityId,focusHref:a.getAttribute('href'),focusSearch:a===input,flags:[...root.querySelectorAll('input[type=checkbox]')].map(e=>e.checked),headers:[...table.querySelectorAll('th')].map(e=>e.textContent),state:root.dataset.queryState,overflow:document.documentElement.scrollWidth>innerWidth};});""")
def position_origin(self):
self.code("await page.locator('main tbody tr a').first().focus();await page.locator('main [data-slot=entity-table]').evaluate(root=>{root.closest('[data-slot=workspace-scroll]').scrollTop=150;root.querySelector('[data-slot=table-container]').scrollLeft=40});return true;")
self.settle()
return self.values()
def open_row(self, section):
self.code("await page.locator('main tbody tr a').first().evaluate(e=>e.click());return true;")
if section=='workstations':
self.code("await page.locator('main h1').waitFor();return true;")
elif section=='work-profiles':
self.code("await page.getByRole('heading',{name:/Entorno actual/}).waitFor();return true;")
else:
self.code("await page.locator('[data-slot=company-context-bar]').waitFor({state:'attached'});return true;")
self.code("await page.locator('[data-slot=workspace-scroll]').evaluate(e=>e.scrollTop=400);return true;")
self.settle()
def returned(self, before, section, explicit=False):
if explicit and section=='workstations':
self.code("const mobile=page.getByRole('link',{name:'Volver a puestos de trabajo',exact:true});if(await mobile.isVisible())await mobile.click();else await page.getByRole('link',{name:'Puestos de trabajo',exact:true}).last().click();return true;")
elif explicit and section=='work-profiles':
self.code("await page.getByRole('button',{name:'Perfiles de usuario',exact:true}).click();return true;")
else:
self.code("await page.goBack();return true;")
self.code("await page.locator('main [data-query-state=ready]').waitFor();return true;")
self.wait_return_focus(before['focusRow'])
after=self.values()
for key in ['url','query','page','sort','rows','focusRow','focusHref','x','y']:
self.assertEqual(before[key],after[key],key)
self.assertFalse(after['overflow'])
return after
def test_41_workstations_return_matrix(self):
for i,(width,height) in enumerate(CONTEXT['SIZES']):
self.resize(width,height)
self.list('workstations',{'q':' L04-PUESTO ','sort':'name','direction':'desc','page':'3'})
before=self.position_origin();self.record('stations-before-'+str(width),before)
self.open_row('workstations')
after=self.returned(before,'workstations',i%2==1)
self.record('stations-return-'+str(width),after)
if width in [1440,390,320]:self.capture('stations-return-'+str(width))
def test_42_profiles_environment_and_configuration(self):
for width,height in CONTEXT['SIZES']:
self.resize(width,height)
self.list('work-profiles',{'q':' L04-PERFIL ','archived':'true','sort':'name','direction':'desc','page':'3'})
before=self.position_origin();self.open_row('work-profiles')
self.assertEqual(parse_qs(urlsplit(self.code('return page.url();')).query).get('archived'),['true'])
self.code("await page.getByRole('button',{name:'Gestionar Aplicaciones del perfil',exact:true}).click();await page.getByRole('button',{name:'Volver al entorno',exact:true}).waitFor();return true;")
self.assertNotIn('q',parse_qs(urlsplit(self.code('return page.url();')).query))
self.code("await page.getByRole('button',{name:'Volver al entorno',exact:true}).click();await page.getByRole('heading',{name:/Entorno actual/}).waitFor();return true;")
after=self.returned(before,'work-profiles',True)
self.record('profiles-return-'+str(width),after)
if width in [1440,390,320]:self.capture('profiles-return-'+str(width))
def test_43_directory_and_internal_section(self):
for width,height in CONTEXT['SIZES']:
self.resize(width,height)
self.directory({'q':' AUDIT ','sort':'name','direction':'desc','page':'2'})
before=self.position_origin();self.open_row('companies')
self.code("if(!(await page.getByRole('link',{name:/^Sedes/}).first().isVisible()))await page.getByRole('button',{name:/^Secciones de/}).first().click();await page.getByRole('link',{name:/^Sedes/}).first().click();await page.locator('main table').waitFor();await page.goBack();return true;")
after=self.returned(before,'companies')
self.record('directory-return-'+str(width),after)
if width in [1920,1024,390]:self.capture('directory-return-'+str(width))
def test_44_reload_invalid_parameters_and_scope(self):
self.resize(1440,900)
for section in ['workstations','work-profiles','companies']:
params={'sort':'name','direction':'desc','page':'3' if section!='companies' else '2'}
(self.directory(params) if section=='companies' else self.list(section,params))
before=self.position_origin();self.code("await page.reload();await page.locator('main [data-query-state=ready]').waitFor();return true;");self.wait_return_focus(before['focusRow'])
after=self.values()
for key in ['url','sort','page','rows','focusRow','x','y']:self.assertEqual(before[key],after[key],section+':'+key)
invalid={'sort':'actions','direction':'sideways','page':'-3','companyId':self.company} if section!='companies' else {'sort':'actions','direction':'sideways','page':'-3'}
(self.directory(invalid) if section=='companies' else self.list(section,invalid))
self.code("await page.waitForFunction(()=>!['sort','direction','page'].some(k=>new URL(location.href).searchParams.has(k)));return true;")
self.assertEqual(self.values()['sort'],'ascending');self.assertEqual(self.values()['page'],'1')
if section!='companies':self.assertEqual(parse_qs(urlsplit(self.values()['url']).query).get('companyId'),[self.company])
self.record(section+'-invalid-canonical',self.values())
def test_45_filters_preferences_and_atomic_clear(self):
self.resize(1440,900)
for section in ['workstations','work-profiles','companies']:
params={'q':' L04 ','sort':'name','direction':'desc','page':'3'}
if section=='companies':params={'q':' AUDIT ','activeOnly':'true','sort':'name','direction':'desc','page':'2'}
if section=='work-profiles':params['archived']='true'
(self.directory(params) if section=='companies' else self.list(section,params))
self.code("await page.getByRole('button',{name:'Columnas',exact:true}).click();await page.getByRole('dialog').getByRole('checkbox').filter({hasText:/Estado/}).count();return true;")
self.code("await page.getByRole('dialog').getByRole('checkbox',{name:/Estado/}).uncheck();await page.keyboard.press('Escape');return true;")
self.code("await page.evaluate(()=>{window.adoptionReplaceCount=0;window.adoptionOriginalReplace=history.replaceState;history.replaceState=function(...args){if(args[2]!==undefined&&new URL(args[2],location.href).href!==location.href)window.adoptionReplaceCount++;return window.adoptionOriginalReplace.apply(this,args)}});await page.getByRole('button',{name:'Limpiar filtros',exact:true}).click();return true;")
self.settle();after=self.values()
count=self.code("return page.evaluate(()=>{const n=window.adoptionReplaceCount;history.replaceState=window.adoptionOriginalReplace;return n});")
self.assertEqual(after['query'],'');self.assertEqual(after['page'],'1');self.assertEqual(after['sort'],'descending');self.assertTrue(after['focusSearch']);self.assertFalse(any('Estado' in h for h in after['headers']))
self.record(section+'-clear',{'state':after,'urlReplacements':count})
self.assertLessEqual(count,1,'Limpiar debe agrupar los cambios de consulta')
self.code("await page.reload();await page.locator('main [data-query-state=ready]').waitFor();return true;")
self.assertFalse(any('Estado' in h for h in self.values()['headers']))
def test_46_dialogs_and_search_contract(self):
self.resize(390,844)
for section in ['workstations','work-profiles']:
self.list(section,{'q':'l04','sort':'name','direction':'desc','page':'3'})
before=self.position_origin()
self.code("await page.getByRole('button',{name:'Editar',exact:true}).first().click();await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
after=self.values()
for key in ['url','query','page','sort','rows']:self.assertEqual(before[key],after[key])
self.assertNotEqual(after['focus'],'BODY')
if section == 'workstations':
detail_origin = self.position_origin()
self.open_row(section)
detail_url = self.code('return page.url();')
self.code("await page.getByRole('button',{name:'Editar puesto',exact:true}).first().click();await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.settle()
self.assertEqual(self.code('return page.url();'), detail_url)
self.assertEqual(self.code("return page.evaluate(()=>document.activeElement.textContent.trim());"), 'Editar puesto')
self.record('station-detail-edit-cancel-return', self.returned(detail_origin,section,True))
self.code("await page.locator('main input[data-quick-search]').fill(' ADMINISTRACION ');return true;");self.settle();self.assertGreater(self.values()['count'],0);self.assertEqual(self.values()['query'],' ADMINISTRACION ')
self.code("await page.locator('main input[data-quick-search]').fill('Pena');return true;");self.settle();self.assertEqual(self.values()['count'],1)
self.record(section+'-search',self.values())
def test_47_removed_and_direct_records(self):
self.resize(390,844)
for section in ['workstations','work-profiles']:
self.list(section,{'q':'l04','sort':'name','direction':'desc','page':'3'})
before=self.position_origin();self.open_row(section)
self.code('page.adoptionRemoved='+json.dumps(before['focusRow'])+";await page.evaluate(()=>{window.dispatchEvent(new Event('offline'));window.dispatchEvent(new Event('online'))});await page.goBack();return true;")
self.code("await page.locator('main [data-query-state=ready]').waitFor();await page.waitForFunction(()=>document.activeElement.hasAttribute('data-quick-search'));return true;")
after=self.values();self.assertFalse(any(r['id']==before['focusRow'] for r in after['rows']));self.assertTrue(after['focusSearch']);self.record(section+'-removed-return',after)
self.code("page.adoptionRemoved=null;return true;")
missing='019a4000-0000-7000-a000-999999999999'
self.code('await page.adoptionGoto('+json.dumps(self.origin+self.base+'/workstations/'+missing+'?sort=name&direction=desc&page=3')+");await page.getByRole('link',{name:'Volver a puestos de trabajo',exact:true}).click();await page.locator('main [data-query-state=ready]').waitFor();return true;")
self.assertEqual(self.values()['page'],'3');self.record('direct-station-fallback',self.values())
self.code('await page.adoptionGoto('+json.dumps(self.origin+self.base+'/work-profiles/'+missing+'?sort=name&direction=desc&page=3')+");await page.locator('main').getByRole('link',{name:'Perfiles de usuario',exact:true}).click();await page.locator('main [data-query-state=ready]').waitFor();return true;")
self.assertEqual(self.values()['page'],'3');self.record('direct-profile-fallback',self.values())
def test_48_loading_refetch_failure_and_isolation(self):
self.resize(1440,900)
for section,endpoint in [('workstations','assets'),('work-profiles','work-profiles')]:
self.list(section,{'sort':'name','direction':'desc','page':'3'})
before=self.position_origin();self.open_row(section)
url=self.origin+'/api/'+endpoint+'/'+self.company
self.code('page.adoptionGateUrl='+json.dumps(url)+";page.adoptionGates=[];page.adoptionStarted=new Promise(r=>page.adoptionStart=r);page.adoptionHold=async route=>{page.adoptionStart();await new Promise(r=>page.adoptionGates.push(r));await route.fallback()};await page.route(page.adoptionGateUrl,page.adoptionHold);await page.evaluate(()=>{window.dispatchEvent(new Event('offline'));window.dispatchEvent(new Event('online'))});await page.goBack();await page.adoptionStarted;await page.locator('[data-query-state=refreshing]').waitFor();return true;")
try:
self.assertEqual(self.values()['page'],'3')
self.code("const response=page.waitForResponse(page.adoptionGateUrl);for(const r of page.adoptionGates)r();await response;return true;");self.wait_return_focus(before['focusRow']);self.assertEqual(self.values()['focusRow'],before['focusRow'])
finally:self.code("for(const r of page.adoptionGates)r();await page.unroute(page.adoptionGateUrl,page.adoptionHold);return true;")
self.record(section+'-refetch-return',self.values())
self.list('workstations',{'q':'L04','sort':'name','direction':'desc','page':'3'})
self.code('await page.locator('+json.dumps('a[href="/companies/'+self.other+'"]')+").first().evaluate(e=>e.click());return true;")
self.code("await page.getByRole('link',{name:/^Puestos de trabajo/}).first().evaluate(e=>e.click());await page.locator('main [data-query-state=ready]').waitFor();return true;")
self.assertEqual(self.values()['query'],'');self.assertEqual(self.values()['page'],'1');self.assertIsNone(self.values().get('focusRow'))
self.record('company-isolation',self.values())
def test_49_history_keyboard_and_language(self):
self.resize(390,844)
for section,label in [('workstations','Historial del puesto'),('work-profiles','Ficha e historial')]:
self.list(section,{'q':'l04','sort':'name','direction':'desc','page':'3'})
before=self.values()
self.code("await page.evaluate(()=>window.adoptionTable=document.querySelector('main table'));await page.locator('main tbody tr').first().getByRole('button',{name:'Acciones',exact:true}).click();return true;")
self.code('await page.getByRole("menuitem",{name:'+json.dumps(label)+',exact:true}).click();await page.getByRole("dialog").waitFor();await page.keyboard.press("Escape");await page.getByRole("dialog").waitFor({state:"hidden"});return true;')
self.assertTrue(self.code("return page.evaluate(()=>window.adoptionTable===document.querySelector('main table'));"))
after=self.values()
for key in ['url','query','page','sort','rows']:self.assertEqual(before[key],after[key])
self.assertNotEqual(after['focus'],'BODY')
before=self.position_origin()
self.code("await page.locator('main tbody tr a').first().focus();await page.keyboard.press('Enter');return true;")
self.returned(before,section)
self.list('work-profiles',{'q':' L04 ','archived':'true','sort':'name','direction':'desc','page':'3'})
before=self.values()
try:
self.code("await page.locator('summary[aria-label=\"Menú de usuario\"]').click();await page.getByRole('combobox',{name:'Idioma',exact:true}).click();await page.getByRole('option',{name:'English',exact:true}).click();await page.locator('summary[aria-label=\"User menu\"]').click();return true;")
after=self.values()
for key in ['url','query','page','sort','rows']:self.assertEqual(before[key],after[key])
self.code("await page.getByRole('button',{name:'Clear filters',exact:true}).focus();await page.keyboard.press('Enter');return true;")
self.assertTrue(self.values()['focusSearch']);self.assertEqual(self.values()['count'],40)
self.capture('profiles-english-keyboard-390');self.record('profiles-english-clear',self.values())
finally:
self.code("if(await page.locator('summary[aria-label=\"User menu\"]').count()){await page.locator('summary[aria-label=\"User menu\"]').click();await page.getByRole('combobox',{name:'Language',exact:true}).click();await page.getByRole('option',{name:'Español',exact:true}).click();await page.locator('summary[aria-label=\"Menú de usuario\"]').click()}return true;")
def test_50_initial_loading_and_query_failures(self):
self.resize(320,640)
errors_before = self.code("return page.adoptionConsole.filter(e=>e.type==='pageerror').length;")
for section,endpoint in [('workstations','assets'),('work-profiles','work-profiles'),('companies','companies')]:
url=self.origin+'/api/'+endpoint+('' if endpoint=='companies' else '/'+self.company)
path='/companies' if section=='companies' else self.base+'/'+section
self.code('page.adoptionGateUrl='+json.dumps(url)+";page.adoptionGates=[];page.adoptionStarted=new Promise(r=>page.adoptionStart=r);page.adoptionHold=async route=>{page.adoptionStart();await new Promise(r=>page.adoptionGates.push(r));await route.fallback()};await page.route(page.adoptionGateUrl,page.adoptionHold);return true;")
try:
self.code('await page.adoptionGoto('+json.dumps(self.origin+path+'?sort=name&direction=desc&page='+('2' if section=='companies' else '3'))+");await page.adoptionStarted;await page.locator('main [data-query-state=loading]').waitFor();return true;")
self.assertIn('page=',self.values()['url']);self.assertEqual(self.values()['state'],'loading')
self.assertGreater(self.code("return page.locator('main [data-slot=entity-table-skeleton-row]').count();"),0)
self.record(section+'-initial-loading',self.values());self.capture(section+'-loading-320')
self.code("const response=page.waitForResponse(page.adoptionGateUrl);for(const r of page.adoptionGates)r();await response;await page.locator('main [data-query-state=ready]').waitFor();return true;")
finally:self.code("for(const r of page.adoptionGates)r();await page.unroute(page.adoptionGateUrl,page.adoptionHold);return true;")
self.code('page.adoptionFailureUrl='+json.dumps(url)+";page.adoptionFailureStatus=503;page.adoptionFailure=route=>route.fulfill({status:page.adoptionFailureStatus,contentType:'application/json',json:{code:page.adoptionFailureStatus===403?'FORBIDDEN':'INTERNAL_ERROR',requestId:'019a4000-0000-7000-a000-000000099001'}});await page.route(page.adoptionFailureUrl,page.adoptionFailure);await page.evaluate(()=>{window.dispatchEvent(new Event('offline'));window.dispatchEvent(new Event('online'))});await page.locator('main [data-query-state=stale]').waitFor();return true;")
try:
stale=self.values();self.assertGreater(len(stale['rows']),0);self.record(section+'-stale',stale)
self.code("page.adoptionFailureStatus=403;await page.locator('main [data-slot=entity-table]').getByRole('button',{name:'Reintentar',exact:true}).click();await page.locator('main [data-query-state=unavailable]').waitFor();return true;")
denied=self.values();self.assertEqual(len(denied['rows']),0);self.assertEqual(self.code("return page.locator('main [data-slot=entity-table]').getByRole('button',{name:'Reintentar',exact:true}).count();"),0)
self.record(section+'-forbidden',denied)
finally:self.code("await page.unroute(page.adoptionFailureUrl,page.adoptionFailure);return true;")
self.code("page.adoptionFailureStatus=503;await page.route(page.adoptionFailureUrl,page.adoptionFailure);return true;")
try:
self.code('await page.adoptionGoto('+json.dumps(self.origin+path+'?sort=name&direction=desc&page='+('2' if section=='companies' else '3'))+");await page.locator('main [data-query-state=unavailable]').waitFor();await page.evaluate(()=>window.adoptionErrorTable=document.querySelector('main table'));return true;")
unavailable=self.values();self.assertEqual(len(unavailable['rows']),0);self.assertIn('page=',unavailable['url']);self.record(section+'-initial-503',unavailable)
self.code("await page.unroute(page.adoptionFailureUrl,page.adoptionFailure);await page.locator('main [data-slot=entity-table]').getByRole('button',{name:'Reintentar',exact:true}).click();await page.locator('main [data-query-state=ready]').waitFor();return true;")
self.assertTrue(self.code("return page.evaluate(()=>window.adoptionErrorTable===document.querySelector('main table'));"));self.assertGreater(len(self.values()['rows']),0)
finally:self.code("await page.unroute(page.adoptionFailureUrl,page.adoptionFailure);return true;")
self.assertEqual(self.code("return page.adoptionConsole.filter(e=>e.type==='pageerror').length;"), errors_before, 'Reintentar no deja promesas rechazadas sin gestionar')
def test_51_directory_removed_and_inaccessible(self):
self.resize(1440,900)
self.directory({'q':'AUDIT','sort':'name','direction':'desc','page':'2'})
before=self.position_origin();self.open_row('companies')
self.code('page.adoption.companies.companies=page.adoption.companies.companies.filter(c=>c.id!=='+json.dumps(before['focusRow'])+');await page.goBack();return true;')
self.code("await page.locator('main [data-query-state=ready]').waitFor();await page.waitForFunction(()=>document.activeElement.hasAttribute('data-quick-search'));return true;")
self.assertTrue(self.values()['focusSearch']);self.assertFalse(any(r['id']==before['focusRow'] for r in self.values()['rows']))
self.record('directory-removed-return',self.values())
self.directory({'q':'AUDIT','sort':'name','direction':'desc','page':'2'})
before=self.position_origin();url=self.origin+'/api/companies/'+before['focusRow']
self.code('page.adoptionDeniedCompanyUrl='+json.dumps(url)+";page.adoptionDeniedCompany=r=>r.fulfill({status:403,contentType:'application/json',json:{code:'FORBIDDEN',requestId:'019a4000-0000-7000-a000-000000099002'}});await page.route(page.adoptionDeniedCompanyUrl,page.adoptionDeniedCompany);await page.locator('main tbody tr a').first().click();await page.getByRole('alert').first().waitFor();return true;")
try:
self.returned(before,'companies');self.record('directory-inaccessible-return',self.values())
finally:self.code("await page.unroute(page.adoptionDeniedCompanyUrl,page.adoptionDeniedCompany);return true;")
def load_tests(loader, tests, pattern):
return unittest.TestSuite(AdoptionUiTests(name) for name in sorted(AdoptionUiTests.__dict__) if name.startswith('test_'))
if __name__ == '__main__':
unittest.main(verbosity=2)