#!/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,...(iw.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)