#!/usr/bin/env python3 """L04.2: catálogos y configuraciones por el MCP autenticado existente. Las ampliaciones son GET sintéticos validados; no inicia servicios ni guarda formularios. """ import json import os from pathlib import Path import runpy import subprocess import unittest from urllib.parse import urlencode, urlsplit, parse_qs ROOT = Path(__file__).resolve().parent.parent ADOPTION = runpy.run_path(str(ROOT / "scripts/adoption-ui.test.py")) class CatalogConfigurationUiTests(ADOPTION["AdoptionUiTests"]): artifact_directory = ROOT / ".artifacts/playwright/l04-2/after" @classmethod def setUpClass(cls): super().setUpClass() cls.code("""const get=async p=>{const r=await page.request.get(page.adoptionOrigin+'/api'+p,{timeout:15000});if(r.status()!==200)throw Error('Fixture GET '+p+': '+r.status());return r.json()}; let sequence=10000;const id=()=> '019a4200-0000-7000-a000-'+String(++sequence).padStart(12,'0'),company=page.adoptionCompany; const actor=page.adoption.inventory.assets[0].history[0],revision=values=>({id:id(),actorId:actor.actorId,actorName:actor.actorName,recordedAt:'2026-03-01T10:00:00.000Z',values:structuredClone(values)}); const catalogs={};for(const family of ['asset','resource']){const global=await get('/'+family+'-catalog/global'),local=page.adoption.snapshots['/'+family+'-catalog/'+company];const extra=[]; for(const kind of family==='asset'?['field','template','relation']:['field','template']){const sample=global.items.find(x=>x.values.kind===kind);if(!sample)throw Error('Falta prototipo '+family+':'+kind);for(let i=1;i<=50;i++){const x=structuredClone(sample);x.id=id();x.companyId=null;x.builtin=false;x.builtinKey=null;delete x.referenceCatalog;x.active=i%5!==0;x.effectiveActive=x.active;x.overridden=false;x.values.name='L042-'+({field:'CAMPO',template:'PLANTILLA',relation:'RELACION'}[kind])+'-'+String(i).padStart(2,'0')+' · Administración '+(i===2?'Pena':'Peña'); if(kind==='template'){const sections=new Map((x.values.sections??[]).map(s=>[s.id,id()]));x.values.sections=x.values.sections?.map(s=>({...s,id:sections.get(s.id)}));x.values.fields=x.values.fields.map(f=>({...f,...(f.sectionId?{sectionId:sections.get(f.sectionId)}:{})}));x.bindings=x.bindings.map(b=>({...b,id:id()}))}extra.push(x)}} catalogs['/'+family+'-catalog/global']={items:[...global.items,...extra]};catalogs['/'+family+'-catalog/'+company]={items:[...local.items,...extra.map((x,i)=>i%2||x.values.kind!=='field'?x:{...x,id:id(),companyId:company})]}} const brands={brands:Array.from({length:50},(_,i)=>({id:id(),name:'L042-MARCA-'+String(i+1).padStart(2,'0')+' · Administración Peña',active:i%5!==0,builtin:false,logoSvg:null}))}; const templates={templates:Array.from({length:50},(_,i)=>{const values={name:'L042-PROCESO-'+String(i+1).padStart(2,'0')+' · Administración Peña',kind:'onboarding',active:i%5!==0,notes:null,instructions:['Comprobar inventario','Confirmar responsabilidades']};return{id:id(),values,history:[revision(values)]}})}; const globalAccess={items:[],reviews:[],coverage:[]};const entry=(values,companyId=null)=>({id:id(),companyId,values,history:[revision(values)]}); for(let i=1;i<=50;i++){globalAccess.items.push(entry({kind:'system',name:'L042-SISTEMA-'+String(i).padStart(2,'0')+' · Administración Peña',active:i%5!==0}));globalAccess.items.push(entry({kind:'resource_type',name:'L042-TIPO-'+String(i).padStart(2,'0')+' · Administración Peña',active:true}))} const type=globalAccess.items.find(x=>x.values.kind==='resource_type'),system=globalAccess.items.find(x=>x.values.kind==='system'&&x.values.active); for(let i=1;i<=50;i++){globalAccess.items.push(entry({kind:'permission',name:'L042-PERMISO-'+String(i).padStart(2,'0')+' · Lectura Álvarez',active:true,resourceTypeId:type.id,description:null}));globalAccess.items.push(entry({kind:'template',name:'L042-ACCESO-'+String(i).padStart(2,'0')+' · Administración Peña',active:true,description:null,systemId:system.id}))} const localAccess=structuredClone(page.adoption.snapshots['/access/'+company]);localAccess.items.push(...globalAccess.items);const environment=entry({kind:'environment',name:'L042-ENTORNO · Administración',active:true,systemId:system.id},company);localAccess.items.push(environment); for(let i=1;i<=50;i++)localAccess.items.push(entry({kind:'account',systemId:system.id,environmentId:environment.id,login:'L042-CUENTA-'+String(i).padStart(2,'0')+'@administracion.example.test',usage:'personal',active:i%5!==0,expiresOn:null,notes:null},company)); page.catalogConfiguration={catalogs,brands,templates,globalAccess,localAccess};const data={...catalogs,'/asset-brands':brands,'/processes/templates':templates,'/access/global':globalAccess,['/access/'+company]:localAccess}; for(const[path,payload]of Object.entries(data))await page.route(page.adoptionOrigin+'/api'+path,r=>r.request().method()==='GET'?r.fulfill({status:200,contentType:'application/json',json:payload}):r.fallback());return true;""") fixture = cls.output / ("catalog-fixtures-" + str(os.getpid()) + ".json") fixture.write_text(json.dumps(cls.code('return page.catalogConfiguration;'), ensure_ascii=False) + '\n') validation = """import{readFile}from'node:fs/promises';import{CatalogResponseSchema as A,BrandsResponseSchema}from'./modules/assets/src/common/index.ts';import{CatalogResponseSchema as R}from'./modules/resources/src/common/index.ts';import{AccessResponseSchema}from'./modules/companies/src/common/dto/shared/access.dto.ts';import{ProcessTemplatesResponseSchema}from'./modules/companies/src/common/processes.ts';const f=JSON.parse(await readFile(process.argv[1],'utf8'));for(const[k,v]of Object.entries(f.catalogs))(k.startsWith('/asset')?A:R).parse(v);BrandsResponseSchema.parse(f.brands);ProcessTemplatesResponseSchema.parse(f.templates);AccessResponseSchema.parse(f.globalAccess);AccessResponseSchema.parse(f.localAccess);console.log('Schemas de catálogos correctos.');""" result = subprocess.run(['node', '--import', 'tsx', '--input-type=module', '-e', validation, str(fixture)], cwd=ROOT, capture_output=True, text=True, timeout=60) (cls.output / ('catalog-validation-' + str(os.getpid()) + '.log')).write_text(result.stdout + result.stderr) if result.returncode: raise RuntimeError('Fixtures de catálogo no válidas; revisar evidencia.') cls.prepare_configurations() @classmethod def prepare_configurations(cls): cls.code(r"""const company=page.adoptionCompany,p=page.adoption.profiles.profiles,people=page.adoption.detail.company.people,access=page.catalogConfiguration.localAccess; let sequence=0;const id=()=> '019a4201-0000-7000-a000-'+String(++sequence).padStart(12,'0'),actor=page.adoption.inventory.assets[0].history[0]; const revision=(values,recordedAt='2026-03-01T10:00:00.000Z')=>({id:id(),actorId:actor.actorId,actorName:actor.actorName,recordedAt,values:structuredClone(values)}); const entry=values=>({id:id(),companyId:company,values,history:[revision(values)]}); const history=values=>{const states=values.state==='removed'?['pending','configured','removal_pending','removed']:values.state==='configured'?['pending','configured']:[values.state];return states.map((state,i)=>revision({...values,state},'2026-03-0'+(i<2?'1':'2')+'T'+(i%2?'11':'10')+':00:00.000Z'))}; const configuration=(values,extra={})=>({...entry(values),history:history(values),needsReview:false,...extra}); const resourceData=page.adoption.snapshots['/resources/'+company],catalog=page.adoption.snapshots['/resource-catalog/'+company],template=(r,key)=>catalog.items.some(t=>t.id===r.values.templateId&&t.builtinKey==='template:'+key); const applications=resourceData.resources.filter(r=>template(r,'application'));if(applications.length<50)throw Error('Faltan 50 aplicaciones de recurso para el escenario de software'); const certificate=resourceData.resources.find(r=>template(r,'certificate')&&r.values.active),printer=page.adoption.inventory.assets.find(a=>a.specification.templateName==='Impresora'&&a.values.status==='operational');if(!certificate||!printer)throw Error('Faltan certificado e impresora reales de la fixture'); const type=access.items.find(x=>x.values.kind==='resource_type'&&x.values.name.startsWith('L042')),permission=access.items.find(x=>x.values.kind==='permission'&&x.values.resourceTypeId===type.id),system=access.items.find(x=>x.values.kind==='system'&&x.values.active),accounts=access.items.filter(x=>x.values.kind==='account'&&x.values.login.startsWith('L042')); const mailboxes={mailboxes:[],uses:[]},mail={configurations:[]},folders={mappings:[]},certificates={configurations:[]},apps={configurations:[]},printers={configurations:[]}; for(let i=0;i<50;i++){const profile=p[i],person=people.find(x=>x.id===profile.values.personId),account=accounts[i],active=profile.values.active,state=active?'configured':'removed';if(!person||!account)throw Error('Perfil sin referencias válidas'); profile.values.accountId=account.id;for(const rev of profile.history)rev.values.accountId=account.id; const period={startDate:'2026-03-01',endDate:active?null:'2026-03-02'};access.items.push(entry({kind:'account_person',accountId:account.id,personId:person.id,...period,configuration:state})); const folderResource=entry({kind:'resource',name:'L042-CARPETA-'+String(i+1).padStart(2,'0')+' · Administración Peña',active,resourceTypeId:type.id,systemId:system.id,environmentId:null,reference:'\\L042-NAS\Area-'+(i+1),notes:null});access.items.push(folderResource); const need=entry({kind:'need',personId:person.id,resourceId:folderResource.id,permissionIds:[permission.id],...period,notes:null});access.items.push(need);access.items.push(entry({kind:'grant',resourceId:folderResource.id,permissionId:permission.id,accountId:account.id,groupId:null,...period,configuration:state}));if(active)access.coverage.push({needId:need.id,covered:true,missingPermissionIds:[]}); const mailbox=entry({address:'l042-mail-'+String(i+1).padStart(2,'0')+'@audit.example.test',kind:'corporate',context:'Administración Peña',accountId:account.id,active,notes:null});mailboxes.mailboxes.push(mailbox);mailboxes.uses.push(entry({mailboxId:mailbox.id,personId:person.id,...period})); mail.configurations.push(configuration({profileId:profile.id,mailboxId:mailbox.id,address:mailbox.values.address,method:'imap',state,notes:null},{legacy:false})); folders.mappings.push(configuration({profileId:profile.id,needId:need.id,accessAccountId:account.id,path:folderResource.values.reference,driveLetter:'Z:',state,notes:null},{permissionsCovered:active,accessAccountCoverage:active?'covered':'unknown'})); apps.configurations.push(configuration({profileId:profile.id,resourceId:applications[0].id,delivery:'installed',version:'2026.1',state,notes:null})); printers.configurations.push(configuration({profileId:profile.id,printerId:printer.id,connection:'network',isDefault:true,state,notes:null})); const expiresId=catalog.items.find(x=>x.builtinKey==='field:expires')?.id,expires=expiresId?certificate.values.characteristics[expiresId]:null; certificates.configurations.push(configuration({profileId:profile.id,resourceId:certificate.id,location:'Almacén personal · Administración Peña',state,notes:null},{expiresOn:typeof expires==='string'?expires:null,expired:false})); } const telework={authorizations:[]},remote={connections:[]};for(let i=0;i<50;i++){const person=people[i],effective=person.active;telework.authorizations.push({...entry({personId:person.id,active:effective,startsOn:'2026-03-01',expiresOn:'2030-12-31',notes:null}),status:effective?'authorized':'revoked',effective,needsReview:false});remote.connections.push(configuration({personId:person.id,sourceKind:'personal_computer',sourceAssetId:null,targetKind:'network',targetId:null,siteId:page.adoption.detail.company.sites[i].id,method:'VPN · L042 Administración',requiresVpn:true,state:effective?'configured':'removed',notes:null},{authorizationEffective:effective}))} const templates=page.catalogConfiguration.templates.templates,processes={processes:templates.map((t,i)=>{const state=i%5===4?'completed':'open',values={personId:p[i].values.personId,templateId:t.id,templateName:t.values.name,kind:t.values.kind,name:'L042-PROCESO-'+String(i+1).padStart(2,'0')+' · Administración Peña',state,notes:null,tasks:t.values.instructions.map(title=>({id:id(),title,status:state==='open'?'pending':'done',notes:null,reference:null}))};return{...entry(values),review:[]}})}; const assetId=p.find(x=>x.values.active).values.assetId,software={configurations:applications.slice(0,50).map(resource=>configuration({assetId,resourceId:resource.id,licenseResourceId:null,version:'2026.1',state:resource.values.active?'configured':'removed',notes:null}))}; const data={['/mailboxes/'+company]:mailboxes,['/mail-configurations/'+company]:mail,['/folder-mappings/'+company]:folders,['/certificate-configurations/'+company]:certificates,['/application-configurations/'+company]:apps,['/printer-configurations/'+company]:printers,['/telework-authorizations/'+company]:telework,['/remote-connections/'+company]:remote,['/processes/'+company]:processes,['/software-installations/'+company]:software,['/access/'+company]:access,['/work-profiles/'+company]:page.adoption.profiles};page.configurationRemoved=null; page.configurationData={data,assetId,profileId:p.find(x=>x.values.active).id,personId:people.find(x=>x.active).id,companyId:company};for(const[path,payload]of Object.entries(data))await page.route(page.adoptionOrigin+'/api'+path,r=>{if(r.request().method()!=='GET')return r.fallback();const json=path==='/processes/'+company?{processes:payload.processes.filter(x=>x.id!==page.configurationRemoved)}:payload;return r.fulfill({status:200,contentType:'application/json',json})});return true;""") fixture = cls.output / ("configuration-fixtures-" + str(os.getpid()) + ".json") fixture.write_text(json.dumps(cls.code('return page.configurationData;'), ensure_ascii=False) + '\n') validation = """import{readFile}from'node:fs/promises';import{MailboxesResponseSchema}from'./modules/companies/src/common/mailboxes.ts';import{MailConfigurationsResponseSchema}from'./modules/companies/src/common/mail-configurations.ts';import{FolderMappingsResponseSchema}from'./modules/companies/src/common/folder-mappings.ts';import{CertificateConfigurationsResponseSchema}from'./modules/companies/src/common/certificate-configurations.ts';import{ApplicationConfigurationsResponseSchema}from'./modules/companies/src/common/application-configurations.ts';import{PrinterConfigurationsResponseSchema}from'./modules/companies/src/common/printer-configurations.ts';import{TeleworkAuthorizationsResponseSchema}from'./modules/companies/src/common/telework-authorizations.ts';import{RemoteConnectionsResponseSchema}from'./modules/companies/src/common/remote-connections.ts';import{ProcessesResponseSchema}from'./modules/companies/src/common/processes.ts';import{SoftwareInstallationsResponseSchema}from'./modules/companies/src/common/software-installations.ts';import{AccessResponseSchema}from'./modules/companies/src/common/dto/shared/access.dto.ts';import{WorkProfilesResponseSchema}from'./modules/companies/src/common/work-profiles.ts';const f=JSON.parse(await readFile(process.argv[1],'utf8'));const schemas={'mailboxes':MailboxesResponseSchema,'mail-configurations':MailConfigurationsResponseSchema,'folder-mappings':FolderMappingsResponseSchema,'certificate-configurations':CertificateConfigurationsResponseSchema,'application-configurations':ApplicationConfigurationsResponseSchema,'printer-configurations':PrinterConfigurationsResponseSchema,'telework-authorizations':TeleworkAuthorizationsResponseSchema,'remote-connections':RemoteConnectionsResponseSchema,'processes':ProcessesResponseSchema,'software-installations':SoftwareInstallationsResponseSchema,'access':AccessResponseSchema,'work-profiles':WorkProfilesResponseSchema};for(const[path,payload]of Object.entries(f.data)){const key=path.split('/')[1];schemas[key].parse(payload)}console.log('Schemas de configuraciones correctos: 50 registros por familia.');""" result = subprocess.run(['node', '--import', 'tsx', '--input-type=module', '-e', validation, str(fixture)], cwd=ROOT, capture_output=True, text=True, timeout=60) (cls.output / ('configuration-validation-' + str(os.getpid()) + '.log')).write_text(result.stdout + result.stderr) if result.returncode: raise RuntimeError('Fixtures de configuración no válidas; revisar evidencia.') def route(self, path, params=None): url = self.origin + path + ('?' + 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 position_origin(self): self.code("await page.locator('main tbody tr').first().locator('a, button').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 edit_cancel(self, scope='main'): self.code('const root=page.locator('+json.dumps(scope)+");const dialogs=await page.getByRole('dialog',{includeHidden:true}).count();for(const button of await root.locator('tbody').getByRole('button',{name:'Editar',exact:true}).all()){if(await button.isEnabled()){await button.click();const editor=page.getByRole('dialog',{includeHidden:true}).nth(dialogs);await editor.waitFor();await page.keyboard.press('Escape');await editor.waitFor({state:'hidden'});return true}}throw Error('No hay una fila elegible para editar');") self.settle() def test_71_configuration_tables_edit_history_and_clear(self): self.resize(1440,900) for section in ['folder-mappings','mail-configurations','certificate-configurations','application-configurations','printer-configurations','remote-connections','telework-authorizations']: self.route(self.base+'/'+section);default_count=self.values()['count'] if section=='remote-connections':self.code("await page.locator('main input[type=checkbox]').check();return true;") self.code("await page.locator('main th[aria-sort] button').first().click();await page.getByRole('button',{name:'Página 3',exact:true}).click();return true;");self.settle() before=self.values();self.assertEqual(before['page'],'3');self.assertEqual(before['sort'],'descending');self.assertEqual(len(before['rows']),10) self.code("await page.evaluate(()=>window.configurationTable=document.querySelector('main table'));return true;") self.edit_cancel();self.assertTrue(self.code("return page.evaluate(()=>window.configurationTable===document.querySelector('main table'));")) self.code("await page.locator('main tbody tr').first().getByRole('button',{name:'Acciones',exact:true}).click();await page.getByRole('menuitem').filter({hasText:/[Hh]istorial/}).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],section+':'+key) self.assertNotEqual(after['focus'],'BODY');self.record(section+'-editor-history',after) self.code("await page.locator('main input[data-quick-search]').fill('zz-sin-coincidencias');return true;");self.settle();self.assertEqual(len(self.values()['rows']),0) self.code("await page.getByRole('button',{name:'Limpiar filtros',exact:true}).click();return true;");self.settle();after=self.values() self.assertEqual(after['query'],'');self.assertEqual(after['page'],'1');self.assertEqual(after['sort'],'descending');self.assertEqual(after['count'],default_count);self.assertTrue(after['focusSearch']) self.record(section+'-clear',after) def test_72_required_profile_and_person_scopes(self): self.resize(390,844) ids=self.code('return {profile:page.configurationData.profileId,person:page.configurationData.personId};') for section in ['folder-mappings','mail-configurations','certificate-configurations','application-configurations','printer-configurations','remote-connections','telework-authorizations']: key='personId' if section in ['remote-connections','telework-authorizations'] else 'profileId';value=ids['person' if key=='personId' else 'profile'] self.route(self.base+'/'+section,{key:value});before=self.values();self.assertGreater(len(before['rows']),0);self.assertLessEqual(before['count'],2) self.code("await page.locator('main input[data-quick-search]').fill('zz-sin-coincidencias');await page.getByRole('button',{name:'Limpiar filtros',exact:true}).click();return true;");self.settle();after=self.values() self.assertEqual(parse_qs(urlsplit(after['url']).query).get(key),[value]);self.assertEqual(after['rows'],before['rows']);self.record(section+'-required-scope',after) self.route(self.base+'/mail-configurations',{'profileId':ids['profile']}) self.code("await page.locator('main input[data-quick-search]').fill('consulta del perfil anterior');return true;");self.settle() self.code('const selector='+json.dumps('a[href="'+self.base+'/mail-configurations"]:visible')+";if(!(await page.locator(selector).count()))await page.getByRole('button',{name:/^Secciones de/}).first().click();await page.locator(selector).first().click();await page.locator('main [data-query-state=ready]').waitFor();return true;") self.assertEqual(self.values()['query'],'');self.assertEqual(self.values()['count'],50);self.record('profile-to-company-scope-reset',self.values()) self.route(self.base+'/work-profiles/'+ids['profile']+'/application-configurations') self.assertEqual(self.values()['count'],1) self.code("await page.locator('main input[data-quick-search]').fill('ADMINISTRACION');return true;");self.settle();self.assertEqual(self.values()['count'],1) self.code("await page.getByRole('button',{name:'Volver al entorno',exact:true}).click();await page.getByRole('heading',{name:/Entorno actual/}).waitFor();return true;") self.assertTrue(self.code("return page.url();").endswith('/work-profiles/'+ids['profile'])) def test_73_configuration_dependencies_refetch_and_permissions(self): self.resize(320,640) for section,dependency in [('mail-configurations','mailboxes'),('folder-mappings','access'),('certificate-configurations','assets'),('application-configurations','resources'),('printer-configurations','work-profiles'),('remote-connections','resources'),('telework-authorizations','telework-authorizations')]: url=self.origin+'/api/'+dependency+'/'+self.company self.code('page.configurationGateUrl='+json.dumps(url)+";page.configurationGates=[];page.configurationStarted=new Promise(r=>page.configurationStart=r);page.configurationHold=async r=>{page.configurationStart();await new Promise(resolve=>page.configurationGates.push(resolve));await r.fallback()};await page.route(page.configurationGateUrl,page.configurationHold);return true;") try: self.code('await page.adoptionGoto('+json.dumps(self.origin+self.base+'/'+section)+");await page.configurationStarted;await page.locator('main [data-query-state=loading]').waitFor();return true;") self.assertGreater(self.code("return page.locator('main [data-slot=entity-table-skeleton-row]').count();"),0);self.record(section+'-dependency-loading',self.values()) self.code("for(const r of page.configurationGates)r();await page.locator('main [data-query-state=ready]').waitFor();await page.evaluate(()=>window.configurationTable=document.querySelector('main table'));return true;") finally:self.code("for(const r of page.configurationGates)r();await page.unroute(page.configurationGateUrl,page.configurationHold);return true;") before=self.values();errors=self.code("return page.adoptionConsole.filter(x=>x.type==='pageerror').length;") self.code('page.configurationErrorUrl='+json.dumps(url)+";page.configurationErrorStatus=503;page.configurationError=r=>r.fulfill({status:page.configurationErrorStatus,contentType:'application/json',json:{code:page.configurationErrorStatus===403?'FORBIDDEN':'INTERNAL_ERROR',requestId:'019a4201-0000-7000-a000-000000099999'}});await page.route(page.configurationErrorUrl,page.configurationError);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: self.assertEqual(before['rows'],self.values()['rows']);self.record(section+'-dependency-stale',self.values()) self.code("await page.unroute(page.configurationErrorUrl,page.configurationError);await page.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.configurationTable===document.querySelector('main table'));"));self.assertTrue(self.values()['focusSearch']) self.code("page.configurationErrorStatus=403;await page.route(page.configurationErrorUrl,page.configurationError);await page.evaluate(()=>{window.dispatchEvent(new Event('offline'));window.dispatchEvent(new Event('online'))});await page.locator('main [data-query-state=unavailable]').waitFor();return true;") self.assertEqual(len(self.values()['rows']),0);self.assertEqual(self.code("return page.getByRole('button',{name:'Reintentar',exact:true}).count();"),0);self.record(section+'-dependency-forbidden',self.values()) self.assertEqual(self.code("return page.adoptionConsole.filter(x=>x.type==='pageerror').length;"),errors) finally:self.code("await page.unroute(page.configurationErrorUrl,page.configurationError);return true;") def test_74_initial_error_empty_and_recovery(self): self.resize(390,844) for section,key in [('mail-configurations','configurations'),('folder-mappings','mappings'),('remote-connections','connections')]: url=self.origin+'/api/'+section+'/'+self.company self.code('page.configurationInitialUrl='+json.dumps(url)+";page.configurationInitial=r=>r.fulfill({status:503,contentType:'application/json',json:{code:'INTERNAL_ERROR',requestId:'019a4201-0000-7000-a000-000000099999'}});await page.route(page.configurationInitialUrl,page.configurationInitial);return true;") try: self.code('await page.adoptionGoto('+json.dumps(self.origin+self.base+'/'+section)+");await page.locator('main [data-query-state=unavailable]').waitFor();await page.evaluate(()=>window.configurationTable=document.querySelector('main table'));return true;") self.assertEqual(len(self.values()['rows']),0);self.record(section+'-initial-error',self.values()) self.code("await page.unroute(page.configurationInitialUrl,page.configurationInitial);await page.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.configurationTable===document.querySelector('main table'));"));self.assertGreater(len(self.values()['rows']),0) finally:self.code("await page.unroute(page.configurationInitialUrl,page.configurationInitial);return true;") self.code('page.configurationEmptyData='+json.dumps({key:[]})+";page.configurationEmpty=r=>r.fulfill({status:200,contentType:'application/json',json:page.configurationEmptyData});await page.route(page.configurationInitialUrl,page.configurationEmpty);return true;") try: self.route(self.base+'/'+section);self.assertEqual(self.values()['count'],0);self.assertEqual(len(self.values()['rows']),0);self.assertEqual(self.code("return page.getByRole('button',{name:'Limpiar filtros',exact:true}).count();"),0) self.code("await page.locator('main input[data-quick-search]').fill('consulta sin datos');return true;");self.settle();self.assertEqual(self.code("return page.getByRole('button',{name:'Limpiar filtros',exact:true}).count();"),1) self.record(section+'-empty-no-results',self.values()) finally:self.code("await page.unroute(page.configurationInitialUrl,page.configurationEmpty);return true;") def test_75_process_detail_return_matrix(self): for i,(width,height) in enumerate(ADOPTION['CONTEXT']['SIZES']): self.resize(width,height);self.route(self.base+'/processes',{'q':' L042 ','closed':'true','sort':'name','direction':'desc','page':'3'}) before=self.position_origin();self.record('process-before-'+str(width),before) self.code("await page.locator('main tbody tr a').first().evaluate(e=>e.click());await page.getByRole('button',{name:'Volver a procesos',exact:true}).waitFor();return true;") if i%2:self.code("await page.getByRole('button',{name:'Volver a procesos',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']);self.settle();after=self.values() for key in ['url','query','sort','page','rows','x','y','focusRow']:self.assertEqual(before[key],after[key],str(width)+':'+key) self.assertFalse(after['overflow']);self.record('process-return-'+str(width),after) if width in [1440,390,320]:self.capture('process-return-'+str(width)) def test_76_process_reload_clear_and_unverified_return(self): self.resize(390,844);path=self.base+'/processes' self.route(path,{'q':'L042','closed':'true','sort':'name','direction':'desc','page':'3'});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']);self.settle() for key in ['url','rows','page','sort','x','y','focusRow']:self.assertEqual(before[key],self.values()[key]) self.code("await page.locator('main tbody tr a').first().evaluate(e=>e.click());await page.getByRole('button',{name:'Volver a procesos',exact:true}).waitFor();return true;") self.code('page.configurationRemoved='+json.dumps(before['focusRow'])+";await page.evaluate(()=>{window.dispatchEvent(new Event('offline'));window.dispatchEvent(new Event('online'))});await page.getByRole('link',{name:'Volver a procesos',exact:true}).waitFor();await page.getByRole('link',{name:'Volver a procesos',exact:true}).click();await page.locator('main [data-query-state=ready]').waitFor();await page.waitForFunction(()=>document.activeElement.hasAttribute('data-quick-search'));return true;") self.assertFalse(any(x['id']==before['focusRow']for x in self.values()['rows']));self.record('process-removed-return',self.values()) self.code("page.configurationRemoved=null;return true;") self.route(path,{'q':'L042','closed':'true','sort':'name','direction':'desc','page':'3'});before=self.position_origin() self.code("await page.locator('main tbody tr a').first().evaluate(e=>e.click());page.processGates=[];page.processStarted=new Promise(r=>page.processStart=r);page.processHold=async r=>{page.processStart();await new Promise(resolve=>page.processGates.push(resolve));await r.fallback()};await page.route(page.adoptionOrigin+'/api/processes/'+page.adoptionCompany,page.processHold);await page.evaluate(()=>{window.dispatchEvent(new Event('offline'));window.dispatchEvent(new Event('online'))});await page.goBack();await page.processStarted;await page.locator('main [data-query-state=refreshing]').waitFor();return true;") try: self.assertEqual(self.values()['page'],'3');self.code("for(const r of page.processGates)r();await page.locator('main [data-query-state=ready]').waitFor();return true;");self.wait_return_focus(before['focusRow']);self.record('process-refetch-return',self.values()) finally:self.code("for(const r of page.processGates)r();await page.unroute(page.adoptionOrigin+'/api/processes/'+page.adoptionCompany,page.processHold);return true;") self.code("await page.getByRole('button',{name:'Limpiar filtros',exact:true}).click();return true;");self.settle();after=self.values() self.assertEqual(after['query'],'');self.assertEqual(after['page'],'1');self.assertEqual(after['sort'],'descending');self.assertEqual(after['count'],40);self.assertTrue(after['focusSearch']);self.record('process-clear',after) self.route(path,{'sort':'actions','direction':'unknown','page':'-3'});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') self.route(path,{'q':'L042-PROCESO-29','sort':'name','direction':'desc'});before=self.position_origin() self.code("await page.locator('main tbody tr a').first().evaluate(e=>e.click());await page.getByRole('button',{name:'Actualizar seguimiento',exact:true}).click();await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});await page.getByRole('button',{name:'Volver a procesos',exact:true}).click();await page.locator('main [data-query-state=ready]').waitFor();return true;");self.wait_return_focus(before['focusRow']);self.assertEqual(before['rows'],self.values()['rows']) missing='019a4201-0000-7000-a000-999999999999' self.code('await page.adoptionGoto('+json.dumps(self.origin+path+'/'+missing+'?sort=name&direction=desc&page=3')+");await page.getByRole('link',{name:'Volver a procesos',exact:true}).click();await page.locator('main [data-query-state=ready]').waitFor();return true;");self.assertEqual(self.values()['page'],'3');self.record('process-direct-fallback',self.values()) def test_77_software_modal_and_related_resource(self): self.resize(1440,900) target=self.code("const asset=page.configurationData.assetId,station=page.adoption.inventory.workstations.find(w=>w.values.computerAssetId===asset);return {asset,station:station.id};") self.code('await page.adoptionGoto('+json.dumps(self.origin+self.base+'/workstations/'+target['station'])+");await page.getByRole('button',{name:'Gestionar software',exact:true}).click();await page.getByRole('dialog').waitFor();await page.getByRole('dialog').locator('[data-query-state=ready]').waitFor();return true;") self.code("const d=page.getByRole('dialog');await d.locator('th[aria-sort] button').first().click();await d.getByRole('button',{name:'Página 3',exact:true}).click();await page.evaluate(()=>window.softwareTable=document.querySelector('[role=dialog] table'));return true;") before=self.code("return page.getByRole('dialog').locator('[data-slot=entity-table]').evaluate(r=>({url:location.pathname+location.search,rows:[...r.querySelectorAll('tbody tr[data-entity-id]')].map(x=>x.dataset.entityId),page:r.querySelector('[aria-current=page]').textContent}));") self.edit_cancel('[role=dialog]') self.assertTrue(self.code("return page.evaluate(()=>window.softwareTable===document.querySelector('[role=dialog] table'));"));self.assertNotEqual(self.code('return page.evaluate(()=>document.activeElement.tagName);'),'BODY') after=self.code("return page.getByRole('dialog').locator('[data-slot=entity-table]').evaluate(r=>({url:location.pathname+location.search,rows:[...r.querySelectorAll('tbody tr[data-entity-id]')].map(x=>x.dataset.entityId),page:r.querySelector('[aria-current=page]').textContent}));") self.assertEqual(before,after);self.record('software-editor-cancel',after);self.capture('software-dialog-1440') href=self.code("return page.getByRole('dialog').locator('tbody a').first().getAttribute('href');") self.code("await page.getByRole('dialog').locator('tbody a').first().click();return true;") self.code('await page.waitForURL('+json.dumps(self.origin+href)+");await page.locator('main').getByRole('button',{name:'Ficha e historial',exact:true}).waitFor();return true;") self.record('software-related-resource',self.code('return {url:page.url(),text:(await page.locator(\'main\').innerText()).slice(0,800)};')) self.code("await page.goBack();await page.getByRole('button',{name:'Gestionar software',exact:true}).waitFor();return true;");self.assertEqual(self.code("return page.getByRole('dialog').count();"),0) def test_78_configuration_capacity_and_language(self): for width,height in ADOPTION['CONTEXT']['SIZES']: self.resize(width,height);self.route(self.base+'/mail-configurations');self.code("await page.getByRole('button',{name:'Página 3',exact:true}).click();return true;");self.settle();value=self.position_origin() self.assertFalse(value['overflow']);self.assertEqual(value['page'],'3');self.record('configuration-capacity-'+str(width),value) if width in [1440,390,320]:self.capture('configuration-capacity-'+str(width)) self.resize(390,844);self.route(self.base+'/mail-configurations',{'profileId':self.code('return page.configurationData.profileId;')}) self.code("await page.locator('main input[data-quick-search]').fill('zz-no-match');return true;");self.settle();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();await page.getByRole('button',{name:'Clear filters',exact:true}).focus();await page.keyboard.press('Enter');return true;");self.settle();after=self.values() self.assertEqual(after['url'],before['url']);self.assertEqual(after['query'],'');self.assertEqual(after['count'],1);self.assertTrue(after['focusSearch']);self.record('configuration-english-clear',after);self.capture('configuration-english-390') 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_61_catalog_routes_and_reload(self): self.resize(1440,900) for path in ['/catalogs/assets',self.base+'/catalogs/assets','/catalogs/resources',self.base+'/catalogs/resources']: for tab in ['templates','fields'] + (['relations'] if path.endswith('assets') else []): query={'tab':tab,'q':' L042 ','sort':'name','direction':'desc','page':'3'} self.route(path,query);before=self.position_origin();self.record(path+'-'+tab+'-before',before);self.record('history-before',self.code('return page.evaluate(()=>history.state);')) self.assertEqual(before['page'],'3');self.assertEqual(before['sort'],'descending');self.assertEqual(len(before['rows']),10) 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();self.record(path+'-'+tab+'-after',after);self.record('history-after',self.code('return page.evaluate(()=>history.state);')) for key in ['url','query','page','sort','rows','x','y','focusRow']:self.assertEqual(before[key],after[key],path+':'+tab+':'+key) self.record(path+'-'+tab+'-reload',after) def test_62_tabs_clear_dialogs_and_keyboard(self): self.resize(390,844) for path in ['/catalogs/assets',self.base+'/catalogs/assets','/catalogs/resources',self.base+'/catalogs/resources']: self.route(path,{'q':'L042','sort':'origin','direction':'desc','page':'3'}) self.code("await page.getByRole('button',{name:'Campos',exact:true}).click();return true;");self.settle();v=self.values() params=parse_qs(urlsplit(v['url']).query) self.assertEqual(params.get('tab'),['fields']);self.assertEqual(v['query'],'');self.assertEqual(v['page'],'1');self.assertEqual(v['sort'],'ascending') self.assertNotIn('direction',params) self.code("await page.locator('main input[data-quick-search]').fill(' ADMINISTRACION ');return true;");self.settle();self.assertEqual(self.values()['count'],50) self.code("await page.locator('main input[data-quick-search]').fill('Pena');return true;");self.settle();self.assertEqual(self.values()['count'],1) before=self.values();self.code("await page.locator('main tbody tr').first().getByRole('button',{name:'Ver',exact:true}).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','rows','page','sort']:self.assertEqual(before[key],after[key]) self.assertNotEqual(after['focus'],'BODY');self.record(path+'-tabs-modal',after) self.code("await page.locator('main input[data-quick-search]').fill('L042-CAMPO-01');return true;");self.settle();before=self.values() self.code("await page.locator('main tbody tr').first().getByRole('button',{name:'Acciones',exact:true}).click();await page.getByRole('menuitem',{name:'Editar',exact:true}).click();await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;") self.assertEqual(before['rows'],self.values()['rows']);self.assertEqual(before['url'],self.values()['url']);self.assertNotEqual(self.values()['focus'],'BODY') def test_63_access_scopes_and_tabs(self): self.resize(1440,900) for path,kind in [('/catalogs/access','system'),(self.base+'/access','account')]: self.route(path,{'kind':kind,'q':'L042','sort':'name','direction':'desc','page':'3'});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']) self.assertEqual(self.values()['rows'],before['rows']);self.assertEqual(self.values()['page'],'3') self.code("await page.getByRole('button',{name:'Permisos disponibles',exact:true}).click();return true;");self.settle() self.assertEqual(self.values()['query'],'');self.assertEqual(self.values()['page'],'1');self.assertEqual(self.values()['sort'],'ascending') self.record(path+'-kind-switch',self.values()) env=self.code('return page.catalogConfiguration.localAccess.items.find(i=>i.values.kind===\'environment\'&&i.values.name.startsWith(\'L042\')).id;') self.route(self.base+'/access',{'kind':'account','environmentId':env,'q':'zz-sin-coincidencias','sort':'name','direction':'desc','page':'3'}) self.assertEqual(len(self.values()['rows']),0) self.code("await page.getByRole('button',{name:'Limpiar filtros',exact:true}).click();return true;");self.settle() self.assertEqual(parse_qs(urlsplit(self.values()['url']).query).get('environmentId'),[env]);self.assertGreater(len(self.values()['rows']),0) def test_64_modal_only_catalogs(self): self.resize(390,844) for path in ['/catalogs/brands','/catalogs/process-templates']: self.route(path);self.code("await page.locator('main input[data-quick-search]').fill('ADMINISTRACION');await page.getByRole('columnheader',{name:'Nombre',exact:true}).getByRole('button').click();await page.getByRole('button',{name:'Página 3',exact:true}).click();return true;");self.settle() before=self.values();history=self.code('return page.evaluate(()=>history.length);') self.code("await page.locator('main tbody tr').first().getByRole('button',{name:'Editar',exact:true}).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.assertEqual(self.code('return page.evaluate(()=>history.length);'),history);self.assertNotEqual(after['focus'],'BODY');self.record(path+'-cancel',after) def test_65_catalog_capacity_and_retired_definitions(self): for width,height in ADOPTION['CONTEXT']['SIZES']: self.resize(width,height) self.route(self.base+'/catalogs/assets',{'tab':'fields','q':'L042','sort':'name','direction':'desc','page':'3'}) value=self.position_origin();self.assertFalse(value['overflow']);self.assertEqual(value['page'],'3') self.record('catalog-capacity-'+str(width),value) if width in [1440,390,320]:self.capture('catalog-capacity-'+str(width)) self.route(self.base+'/catalogs/assets',{'tab':'fields','q':'L042-CAMPO-05'}) self.assertIn('Inactivo',self.code("return page.locator('main tbody').innerText();")) self.record('retired-definition',self.values()) def test_66_catalog_refetch_and_permissions(self): self.resize(320,640) for path,endpoint in [('/catalogs/assets','/asset-catalog/global'),('/catalogs/resources','/resource-catalog/global'),('/catalogs/brands','/asset-brands'),('/catalogs/process-templates','/processes/templates'),('/catalogs/access','/access/global')]: self.route(path);before=self.values();errors=self.code("return page.adoptionConsole.filter(x=>x.type==='pageerror').length;") self.code('page.catalogFailureUrl='+json.dumps(self.origin+'/api'+endpoint)+";page.catalogFailureStatus=503;page.catalogFailure=r=>r.fulfill({status:page.catalogFailureStatus,contentType:'application/json',json:{code:page.catalogFailureStatus===403?'FORBIDDEN':'INTERNAL_ERROR',requestId:'019a4200-0000-7000-a000-000000099999'}});await page.route(page.catalogFailureUrl,page.catalogFailure);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: self.assertEqual(before['rows'],self.values()['rows']);self.record(path+'-stale',self.values()) self.code("page.catalogFailureStatus=403;await page.getByRole('button',{name:'Reintentar',exact:true}).click();await page.locator('main [data-query-state=unavailable]').waitFor();return true;") self.assertEqual(len(self.values()['rows']),0);self.assertEqual(self.code("return page.getByRole('button',{name:'Reintentar',exact:true}).count();"),0) self.assertEqual(self.code("return page.adoptionConsole.filter(x=>x.type==='pageerror').length;"),errors) self.record(path+'-forbidden',self.values()) finally:self.code("await page.unroute(page.catalogFailureUrl,page.catalogFailure);return true;") def test_67_font_arrival_preserves_saved_scroll(self): self.resize(1440,900) self.route('/catalogs/assets',{'q':'L042','sort':'name','direction':'desc','page':'3'}) before=self.position_origin() self.code("page.catalogFonts=[];page.catalogFontStarted=new Promise(r=>page.catalogFontStart=r);page.catalogFontHold=async route=>{page.catalogFontStart();await new Promise(r=>page.catalogFonts.push(r));await route.fallback()};await page.route('**/*.woff2',page.catalogFontHold);return true;") try: self.code("await page.reload({waitUntil:'domcontentloaded'});await page.catalogFontStarted;await page.locator('main [data-query-state=ready]').waitFor();return true;") self.assertEqual(self.code("return page.evaluate(()=>document.fonts.status);"),'loading') self.assertIsNone(self.values().get('focusRow')) self.record('font-retained',self.values()) self.code("for(const r of page.catalogFonts)r();await page.evaluate(()=>document.fonts.ready);return true;") self.wait_return_focus(before['focusRow']);self.settle();after=self.values() for key in ['url','page','sort','rows','x','y','focusRow']:self.assertEqual(before[key],after[key],key) self.record('font-arrival-return',after) finally:self.code("for(const r of page.catalogFonts)r();await page.unroute('**/*.woff2',page.catalogFontHold);return true;") def test_68_catalog_columns_preferences_and_invalid_parameters(self): self.resize(1440,900) for path in ['/catalogs/assets','/catalogs/resources']: self.route(path,{'q':'L042','sort':'name','direction':'desc','page':'3'}) self.code("await page.getByRole('button',{name:'Columnas',exact:true}).click();await page.getByRole('dialog').getByRole('checkbox',{name:/Origen/}).uncheck();await page.keyboard.press('Escape');await page.reload();await page.locator('main [data-query-state=ready]').waitFor();return true;") self.assertFalse(any('Origen' in x for x in self.values()['headers'])) self.code("await page.getByRole('button',{name:'Campos',exact:true}).click();return true;");self.settle() self.assertTrue(any('Origen' in x for x in self.values()['headers'])) self.route(path,{'sort':'actions','direction':'sideways','page':'-3'}) 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') self.record(path+'-columns-invalid',self.values()) def test_69_container_query_delays_restore(self): self.resize(1440,900) self.route('/catalogs/assets',{'q':'L042','sort':'name','direction':'desc','page':'3'}) before=self.position_origin() self.code("page.catalogContainerGates=[];page.catalogContainerStarted=new Promise(r=>page.catalogContainerStart=r);page.catalogContainerHold=async route=>{page.catalogContainerStart();await new Promise(r=>page.catalogContainerGates.push(r));await route.fallback()};await page.route(page.adoptionOrigin+'/api/companies',page.catalogContainerHold);return true;") try: self.code("await page.reload({waitUntil:'domcontentloaded'});await page.catalogContainerStarted;await page.locator('main [data-query-state=ready]').waitFor();return true;") self.assertEqual(self.code("return page.locator('[data-slot=workspace-scroll] > p[role=status]').count();"),1) self.assertIsNone(self.values().get('focusRow'));self.record('container-pending',self.values()) self.code("for(const r of page.catalogContainerGates)r();await page.locator('[data-slot=workspace-scroll] > p[role=status]').waitFor({state:'hidden'});return true;") self.wait_return_focus(before['focusRow']);self.settle();after=self.values() for key in ['url','page','sort','rows','x','y','focusRow']:self.assertEqual(before[key],after[key],key) self.record('container-ready-return',after) finally:self.code("for(const r of page.catalogContainerGates)r();await page.unroute(page.adoptionOrigin+'/api/companies',page.catalogContainerHold);return true;") def load_tests(loader, tests, pattern): return unittest.TestSuite(CatalogConfigurationUiTests(name) for name in sorted(CatalogConfigurationUiTests.__dict__) if name.startswith('test_')) if __name__ == '__main__': unittest.main(verbosity=2)