#!/usr/bin/env python3 """L03.1: continuidad URL, History y DOM mediante Playwright MCP autenticado. Requiere visual-audit.py y su preparador de volumen. No inicia servicios ni escribe entidades. Las retiradas de registros y las demoras se simulan únicamente en respuestas GET. """ import contextlib import json import os from pathlib import Path import runpy import tomllib import unittest from urllib.parse import urlencode,urlparse ROOT=Path(__file__).resolve().parent.parent AUDIT=runpy.run_path(str(ROOT/'scripts/visual-audit.py')) SIZES=[(1920,1080),(1440,900),(1366,768),(1024,768),(768,1024),(390,844),(320,640)] class ListContextUiTests(unittest.TestCase): @classmethod def setUpClass(cls): value=os.environ.get('METIS_PLAYWRIGHT_STORAGE_STATE') if not value:raise unittest.SkipTest('Requiere sesión sintética activa.') state=(ROOT/value).resolve() if not state.is_relative_to(ROOT/'.artifacts/playwright') or state.stat().st_mode&0o077:raise RuntimeError('Requiere sesión privada del preparador.') report=json.loads((state.parent/'report.json').read_text()) if urlparse(report['origin']).hostname!='localhost' or report.get('ordinaryUser') is not True:raise RuntimeError('Requiere instalación desechable ordinaria.') cls.origin=report['origin'];cls.company=report['companyId'];cls.base='/companies/'+cls.company cls.output=ROOT/'.artifacts/playwright/l03-1/after';cls.output.mkdir(parents=True,exist_ok=True);cls.results=[];cls.measurements=cls.output/('measurements-'+str(os.getpid())+'.json') cls.stack=contextlib.ExitStack();cls.addClassCleanup(cls.stack.close) config=tomllib.loads((ROOT/'.codex/config.toml').read_text())['mcp_servers']['playwright'] cls.client=cls.stack.enter_context(AUDIT['browser'](config,state)) info=cls.code('const r=await page.request.get('+json.dumps(cls.origin+'/api/companies')+');if(r.status()!==200)throw Error("Sesión no activa");return r.json();') cls.other=next(c['id']for c in info['companies']if c['id']!=cls.company) templates=cls.code('return (await (await page.request.get('+json.dumps(cls.origin+'/api/asset-catalog/'+cls.company)+')).json()).items;') cls.laptop=next(t['id']for t in templates if t['values']['kind']=='template' and t['values']['name']=='Portátil') cls.code("page.blockedWrites=[];await page.route('**/api/**',r=>{if(['GET','HEAD','OPTIONS'].includes(r.request().method()))return r.continue();page.blockedWrites.push(r.request().method());return r.abort()});return true;") @classmethod def code(cls,body): try:return AUDIT['result_json'](cls.client.tool('browser_run_code_unsafe',{'code':'async(page)=>{'+body+'}'})) except RuntimeError as error: (cls.output/('tool-error-'+str(os.getpid())+'.txt')).write_text(str(error));raise @classmethod def tearDownClass(cls): (cls.output/('console-'+str(os.getpid())+'.json')).write_text(json.dumps(AUDIT['tool'](cls.client,'browser_console_messages',{'level':'warning'}),ensure_ascii=False,indent=2)+'\n') (cls.output/('blocked-writes-'+str(os.getpid())+'.json')).write_text(json.dumps(cls.code('return page.blockedWrites;'))+'\n') def record(self,name,value): self.results.append({'scenario':name,'value':value});self.measurements.write_text(json.dumps(self.results,ensure_ascii=False,indent=2)+'\n') def settle(self): self.code("await page.evaluate(async()=>{await document.fonts.ready;await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)));await Promise.all(document.getAnimations().filter(a=>a.effect?.getTiming().iterations!==Infinity).map(a=>a.finished.catch(()=>{})))});return true;") def resize(self,w,h): AUDIT['tool'](self.client,'browser_resize',{'width':w,'height':h});self.settle() def ready(self,section='assets'): try: if section.split('/')[0] in ['assets','resources']:self.code("await page.locator('[data-slot=entity-table][data-query-state=ready]').waitFor();return true;") else:self.code("await page.locator('main table').waitFor();return true;") except RuntimeError: self.record('ready-timeout',self.code("return page.evaluate(()=>({url:location.pathname+location.search,tables:[...document.querySelectorAll('[data-slot=entity-table]')].map(e=>({state:e.dataset.queryState,visible:e.getClientRects().length>0})),alerts:[...document.querySelectorAll('[role=alert]')].map(e=>e.textContent),body:document.body.innerText.slice(0,1800)}));"));self.capture('ready-timeout');raise self.settle() def navigate(self,section='assets',params=None): suffix='?'+urlencode(params)if params else '' self.code('await page.goto('+json.dumps(self.origin+self.base+'/'+section+suffix)+",{waitUntil:'domcontentloaded'});return true;");self.ready(section) def asset_query(self):return {'q':'AUDIT-ASSET','template':self.laptop,'sort':'name','direction':'desc','page':'3'} def metrics(self): return self.code("""return page.locator('main table').first().evaluate(table=>{ const root=table.closest('[data-slot=entity-table]'),v=table.closest('[data-slot=workspace-scroll]'),x=table.closest('[data-slot=table-container]'),active=document.activeElement; return {url:location.pathname+location.search,sort:table.querySelector('th[aria-sort]')?.getAttribute('aria-sort'),page:root.querySelector('[aria-current=page]')?.textContent,search:root.querySelector('input[data-quick-search]')?.value,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,focus:active.tagName,focusRow:active.closest('[data-entity-id]')?.dataset.entityId,focusSearch:active.hasAttribute('data-quick-search'),activeTab:[...document.querySelectorAll('main [role=tab][aria-selected=true]')].map(e=>e.textContent),contacts:[...root.querySelectorAll('[role=checkbox]')].map(e=>e.getAttribute('aria-checked')),overflow:document.documentElement.scrollWidth>innerWidth};});""") def capture(self,name): AUDIT['capture'](self.client,self.output,name) snapshot=AUDIT['tool'](self.client,'browser_snapshot',{}) (self.output/(name+'.txt')).write_text('\n'.join(c['text']for c in snapshot['content']if c.get('type')=='text')) def position(self): self.code("await page.locator('main tbody tr a').first().focus();await page.evaluate(()=>{document.querySelector('[data-slot=workspace-scroll]').scrollTop=150;document.querySelector('main table').closest('[data-slot=table-container]').scrollLeft=40});return true;");self.settle();return self.metrics() def open_asset(self): self.code("await page.locator('main tbody tr a').first().evaluate(e=>e.click());await page.locator('main h1').waitFor();await page.locator('[data-slot=workspace-scroll]').evaluate(e=>e.scrollTop=500);return true;");self.settle() def wait_return_focus(self,identifier): self.code('await page.waitForFunction(id=>document.activeElement.closest("[data-entity-id]")?.dataset.entityId===id,'+json.dumps(identifier)+');return true;');self.settle() def test_01_assets_history_and_explicit_return_matrix(self): for i,(w,h)in enumerate(SIZES): self.resize(w,h);self.navigate();history_length=self.code("return page.evaluate(()=>history.length);");self.code("await page.getByRole('tab',{name:'Portátil',exact:true}).click();await page.locator('main input[data-quick-search]').fill('AUDIT-ASSET');await page.getByRole('columnheader',{name:'Nombre',exact:true}).getByRole('button').click();await page.getByRole('button',{name:'Página 3',exact:true}).click();return true;") before=self.position();self.assertEqual(self.code('return page.evaluate(()=>history.length);'),history_length);self.record('before-'+str(w),before);self.open_asset() if i%2:self.code("await page.getByRole('link',{name:'Volver a activos',exact:true}).click();return true;") else:self.code('await page.goBack();return true;') self.ready();self.wait_return_focus(before['focusRow']);after=self.metrics();self.record('returned-'+str(w),after) for key in ['url','sort','page','search','rows','activeTab','x','y','focusRow']:self.assertEqual(before[key],after[key],str(w)+' '+key) self.assertFalse(after['overflow']);self.assertEqual(after['page'],'3') if w in [1920,1440,390,320]:self.capture('returned-'+str(w)) if w==1440: self.code('await page.goForward();await page.locator("main h1").waitFor();await page.goBack();return true;');self.ready();self.wait_return_focus(before['focusRow']);self.assertEqual(self.metrics()['rows'],before['rows']) def test_02_reload_shared_url_and_preferences(self): self.resize(1440,900);self.navigate(params=self.asset_query());before=self.metrics();self.assertEqual(before['page'],'3');self.assertEqual(before['sort'],'descending') self.code("await page.getByRole('button',{name:'Columnas',exact:true}).click();await page.getByRole('checkbox',{name:'Sede',exact:true}).uncheck();await page.keyboard.press('Escape');await page.reload();return true;");self.ready();after=self.metrics();self.assertEqual(after['rows'],before['rows']);self.assertEqual(after['page'],'3');self.assertEqual(after['sort'],'descending');self.assertEqual(self.code("return page.getByRole('columnheader',{name:'Sede',exact:true}).count();"),0) shared=self.code('return page.url();');self.code('const other=await page.context().newPage();await other.goto('+json.dumps(shared)+");await other.locator('[data-query-state=ready]').waitFor();page.sharedResult=await other.locator('main table').evaluate(e=>({sort:e.querySelector('th[aria-sort]')?.getAttribute('aria-sort'),first:e.querySelector('tbody a')?.textContent,page:e.closest('[data-slot=entity-table]').querySelector('[aria-current=page]')?.textContent}));await other.close();return page.sharedResult;");self.record('shared-url',self.code('return page.sharedResult;'));self.assertEqual(self.code('return page.sharedResult.page;'),'3');self.assertEqual(self.code('return page.sharedResult.first;'),before['rows'][0]['name']);self.record('reload',after) def test_03_invalid_parameters_and_reduced_results(self): self.resize(1366,768) for params,sort,page in [({'page':'-1','sort':'missing','direction':'other'},'ascending','1'),({'page':'1.5'},'ascending','1'),({'page':'999'},'ascending','6'),({'page':'9999999999999999999'},'ascending','1'),({'template':'invalid','page':'3'},'ascending','1')]: self.navigate(params=params);value=self.metrics();self.assertEqual(value['sort'],sort);self.assertEqual(value['page'],page);self.assertGreater(len(value['rows']),0);self.record('invalid-'+str(params),value) self.navigate(params=self.asset_query());length=self.code('return page.evaluate(()=>history.length);');self.code("await page.locator('main input[data-quick-search]').fill('AUDIT-ASSET-01');return true;");self.settle();value=self.metrics();self.assertEqual(value['page'],'1');self.assertEqual(len(value['rows']),1);self.assertEqual(value['sort'],'descending');self.assertEqual(self.code('return page.evaluate(()=>history.length);'),length) self.code("await page.locator('main input[data-quick-search]').fill('zzzz-sin-resultados');return true;");self.settle();self.assertEqual(len(self.metrics()['rows']),0);self.code("await page.getByRole('button',{name:'Limpiar filtros',exact:true}).click();return true;");self.settle();self.assertEqual(self.metrics()['page'],'1');self.assertGreater(len(self.metrics()['rows']),0) def test_04_people_contacts_detail_and_reload(self): self.resize(390,844);self.navigate('people');self.code("await page.locator('main input[data-quick-search]').fill('AUDIT-PERSON');await page.getByRole('checkbox',{name:'Solo contactos',exact:true}).check();await page.getByRole('columnheader',{name:'Nombre',exact:true}).getByRole('button').click();await page.getByRole('button',{name:'Página 2',exact:true}).click();return true;") before=self.position();self.record('people-before',before);self.code("await page.locator('main tbody tr a').first().evaluate(e=>e.click());await page.getByRole('button',{name:'Volver a personas',exact:true}).waitFor();return true;");self.capture('person-detail-390');self.code("await page.getByRole('button',{name:'Volver a personas',exact:true}).click();return true;");self.ready('people');self.wait_return_focus(before['focusRow']);after=self.metrics();self.record('people-returned',after) for key in ['url','sort','page','search','rows','contacts','focusRow']:self.assertEqual(before[key],after[key],key) self.code('await page.reload();return true;');self.ready('people');self.assertEqual(self.metrics()['rows'],before['rows']);self.assertEqual(self.metrics()['contacts'],['true']);self.capture('people-returned-390') def test_05_resources_context_and_explicit_dialog(self): for w,h in [(1920,1080),(390,844)]: self.resize(w,h);self.navigate('resources',{'q':'AUDIT-VOLUME','sort':'name','direction':'desc','page':'3'});before=self.position();self.code("await page.evaluate(()=>window.contextTable=document.querySelector('main table'));await page.locator('main tbody tr a').first().evaluate(e=>e.click());return true;");self.settle() if w==1920: self.code("await page.locator('[data-slot=selection-panel]').waitFor();await page.locator('[data-slot=selection-panel]').getByRole('button',{name:'Ficha e historial',exact:true}).click();await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;") else:self.code("await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;") self.settle();after=self.metrics();self.record('resources-returned-'+str(w),after);self.assertEqual(after['sort'],before['sort']);self.assertEqual(after['page'],before['page']);self.assertEqual(after['rows'],before['rows']);self.assertTrue(self.code('return page.evaluate(()=>window.contextTable===document.querySelector("main table"));'));self.capture('resources-context-'+str(w)) if w==1920: selected_url=self.code('return page.url();');self.code("await page.getByRole('button',{name:'Cerrar panel del recurso',exact:true}).click();return true;");self.settle();self.assertEqual(self.code('return page.url();'),selected_url);self.assertEqual(self.metrics()['page'],'3') else: selected=before['rows'][0]['id'];self.navigate('resources/'+selected,{'q':'AUDIT-VOLUME','sort':'name','direction':'desc','page':'3'});self.code("await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;");self.ready('resources');self.assertEqual(self.metrics()['page'],'3');self.assertEqual(self.metrics()['search'],'AUDIT-VOLUME') def test_06_removed_record_and_return_during_refetch(self): self.resize(1440,900);self.navigate(params=self.asset_query());before=self.position();self.open_asset();endpoint=self.origin+'/api/assets/'+self.company self.code('page.removedId='+json.dumps(before['focusRow'])+';page.removedUrl='+json.dumps(endpoint)+";await page.route(page.removedUrl,async r=>{const response=await r.fetch();const data=await response.json();data.assets=data.assets.filter(a=>a.id!==page.removedId);await r.fulfill({response,json:data})});await page.goBack();return true;");self.ready();self.code("await page.waitForFunction(()=>document.activeElement.hasAttribute('data-quick-search'));return true;");after=self.metrics();self.assertTrue(after['focusSearch']);self.assertFalse(any(r['id']==before['focusRow']for r in after['rows']));self.record('removed-return',after);self.capture('removed-return');self.code('await page.unroute(page.removedUrl);return true;') self.navigate(params=self.asset_query());before=self.position();self.open_asset() self.code('page.heldUrl='+json.dumps(endpoint)+";page.held=[];page.started=new Promise(resolve=>page.onStart=resolve);await page.route(page.heldUrl,async r=>{page.onStart();await new Promise(resolve=>page.held.push(resolve));await r.continue()});await page.goBack();await page.started;await page.locator('[data-query-state=refreshing]').waitFor();return true;");pending=self.metrics();self.assertEqual(pending['page'],'3');self.assertEqual(pending['sort'],'descending');self.record('return-refetch-pending',pending) self.code("const response=page.waitForResponse(page.heldUrl);for(const release of page.held)release();await response;await page.unroute(page.heldUrl);return true;");self.ready();self.wait_return_focus(before['focusRow']);self.assertEqual(self.metrics()['y'],before['y']);self.record('return-refetch-completed',self.metrics()) def test_07_initial_loading_on_reload_preserves_page(self): self.resize(320,640);self.navigate(params=self.asset_query());before=self.position();self.open_asset();self.code('await page.goBack();return true;');self.ready();self.wait_return_focus(before['focusRow']) endpoint=self.origin+'/api/assets/'+self.company self.code('page.heldUrl='+json.dumps(endpoint)+";page.held=[];page.started=new Promise(resolve=>page.onStart=resolve);await page.route(page.heldUrl,async r=>{page.onStart();await new Promise(resolve=>page.held.push(resolve));await r.continue()});await page.reload({waitUntil:'domcontentloaded'});await page.started;await page.locator('[data-query-state=loading]').waitFor();return true;") pending=self.metrics();self.assertIn('page=3',pending['url']);self.assertGreater(self.code("return page.locator('[data-slot=entity-table-skeleton-row]').count();"),0);self.record('reload-loading',pending);self.capture('reload-loading-320') self.code("const response=page.waitForResponse(page.heldUrl);for(const release of page.held)release();await response;await page.unroute(page.heldUrl);return true;");self.ready();self.wait_return_focus(before['focusRow']);after=self.metrics();self.assertEqual(after['page'],'3');self.assertEqual(after['x'],before['x']);self.assertEqual(after['y'],before['y']);self.record('reload-restored',after) def test_08_company_isolation_and_cancelled_editor(self): self.resize(1920,1080);self.navigate(params=self.asset_query());before=self.position();self.code('await page.locator('+json.dumps('a[href="/companies/'+self.other+'"]')+").first().click();return true;");self.code("await page.getByRole('link',{name:/^Activos/}).click();await page.locator('[data-query-state=ready]').waitFor();return true;");self.settle();other=self.metrics();self.assertEqual(other['page'],'1');self.assertEqual(other['search'],'');self.assertEqual(other['x'],0);self.assertEqual(other['y'],0);self.record('company-isolation',other) self.code('await page.goBack();await page.goBack();return true;');self.ready();self.wait_return_focus(before['focusRow']);self.assertEqual(self.metrics()['rows'],before['rows']) self.code("await page.getByRole('button',{name:'Editar',exact:true}).first().focus();await page.evaluate(()=>window.editTrigger=document.activeElement);await page.keyboard.press('Enter');await page.getByRole('dialog').waitFor();return true;");url=self.code('return page.url();');self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;");self.settle();self.assertEqual(self.code('return page.url();'),url);self.assertTrue(self.code('return page.evaluate(()=>document.activeElement===window.editTrigger);'));self.assertEqual(self.metrics()['page'],'3') def test_09_dynamic_sort_and_disappearing_column(self): self.resize(1440,900) field=self.code('const inventory=await(await page.request.get('+json.dumps(self.origin+'/api/assets/'+self.company)+')).json();const catalog=await(await page.request.get('+json.dumps(self.origin+'/api/asset-catalog/'+self.company)+")).json();const used=new Set(inventory.assets.flatMap(a=>a.specification.fields.map(f=>f.fieldId)));const field=catalog.items.find(i=>i.values.kind==='field'&&i.effectiveActive&&!used.has(i.id));if(!field)throw Error('Falta campo de catálogo no utilizado en las instantáneas');return {id:field.id,name:field.values.name};") key='characteristic-'+field['id'];self.navigate(params={'sort':key,'direction':'desc','page':'3'});self.assertEqual(self.code("return new URL(page.url()).searchParams.get('sort');"),key);self.assertEqual(self.metrics()['page'],'3');self.assertIn('Descendente',self.code("return page.getByRole('combobox',{name:'Ordenar por',exact:true}).textContent();"));self.record('dynamic-sort',self.metrics()) self.code('page.catalogUrl='+json.dumps(self.origin+'/api/asset-catalog/'+self.company)+';page.removedField='+json.dumps(field['id'])+";await page.route(page.catalogUrl,async r=>{const response=await r.fetch();const data=await response.json();data.items=data.items.filter(i=>i.id!==page.removedField);await r.fulfill({response,json:data})});await page.reload();return true;");self.ready();self.code("await page.waitForFunction(()=>!new URL(location.href).searchParams.has('sort'));return true;");self.assertEqual(self.metrics()['sort'],'ascending');self.assertEqual(self.metrics()['page'],'3');self.record('dynamic-field-removed',self.metrics());self.code('await page.unroute(page.catalogUrl);return true;') def test_10_failed_return_does_not_certify_cached_record(self): self.resize(1440,900);self.navigate(params=self.asset_query());before=self.position();self.open_asset() self.code('page.failedUrl='+json.dumps(self.origin+'/api/assets/'+self.company)+";await page.route(page.failedUrl,r=>r.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));await page.goBack();await page.locator('[data-query-state=stale]').waitFor();await page.waitForFunction(()=>document.activeElement.hasAttribute('data-quick-search'));return true;");after=self.metrics();self.assertTrue(after['focusSearch']);self.assertEqual(after['page'],'3');self.assertEqual(after['rows'],before['rows']);self.record('return-503-unverified',after);self.capture('return-503-unverified');self.code('await page.unroute(page.failedUrl);return true;') def test_11_shrinking_results_clamp_return(self): self.resize(390,844);self.navigate(params={'sort':'name','direction':'desc','page':'6'});before=self.position();self.open_asset() self.code('page.reducedUrl='+json.dumps(self.origin+'/api/assets/'+self.company)+";await page.route(page.reducedUrl,async r=>{const response=await r.fetch();const data=await response.json();data.assets=data.assets.slice(0,8);await r.fulfill({response,json:data})});await page.goBack();return true;");self.ready();self.code("await page.waitForFunction(()=>!new URL(location.href).searchParams.has('page'));return true;");self.settle();after=self.metrics();self.assertEqual(after['page'],'1');self.assertEqual(len(after['rows']),8) if any(row['id']==before['focusRow']for row in after['rows']):self.wait_return_focus(before['focusRow']) else:self.code("await page.waitForFunction(()=>document.activeElement.hasAttribute('data-quick-search'));return true;") self.assertLessEqual(self.metrics()['y'],before['y']);self.record('shrinking-return',self.metrics());self.capture('shrinking-return-390');self.code('await page.unroute(page.reducedUrl);return true;') def test_12_people_unverified_company_response(self): self.resize(1440,900);self.navigate('people',{'q':'AUDIT-PERSON','contactsOnly':'true','sort':'name','direction':'desc','page':'2'});before=self.position();self.code("await page.locator('main tbody tr a').first().evaluate(e=>e.click());await page.getByRole('button',{name:'Volver a personas',exact:true}).waitFor();return true;") self.code('page.companyUrl='+json.dumps(self.origin+'/api/companies/'+self.company)+";await page.route(page.companyUrl,r=>r.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));const response=page.waitForResponse(page.companyUrl);await page.evaluate(()=>{window.dispatchEvent(new Event('offline'));window.dispatchEvent(new Event('online'))});await response;return true;");self.settle();self.code("await page.getByRole('button',{name:'Volver a personas',exact:true}).click();await page.waitForFunction(()=>document.activeElement.hasAttribute('data-quick-search'));return true;");after=self.metrics();self.assertTrue(after['focusSearch']);self.assertEqual(after['rows'],before['rows']);self.assertEqual(after['page'],'2');self.record('people-unverified-return',after);self.capture('people-unverified-return');self.code('await page.unroute(page.companyUrl);return true;') def test_13_page_size_preference_and_controlled_page(self): self.resize(1440,900);self.navigate(params={'sort':'name','direction':'desc','page':'3'}) self.code("await page.getByRole('combobox',{name:'Elementos por página',exact:true}).click();await page.getByRole('option',{name:'25',exact:true}).click();return true;");self.settle();value=self.metrics();self.assertEqual(value['page'],'1');self.assertEqual(len(value['rows']),25);self.assertEqual(self.code("return page.evaluate(()=>localStorage.getItem('entity-page-size'));"),'25') self.navigate(params={'sort':'name','direction':'desc','page':'3'});self.assertEqual(self.metrics()['page'],'3');self.assertEqual(len(self.metrics()['rows']),1);self.code('await page.reload();return true;');self.ready();self.assertEqual(len(self.metrics()['rows']),1);self.assertEqual(self.metrics()['page'],'3');self.record('page-size-reload',self.metrics()) self.code("await page.getByRole('combobox',{name:'Elementos por página',exact:true}).click();await page.getByRole('option',{name:'50',exact:true}).click();return true;");self.settle();self.assertEqual(self.metrics()['page'],'1');self.assertEqual(len(self.metrics()['rows']),50) self.code("await page.getByRole('combobox',{name:'Elementos por página',exact:true}).click();await page.getByRole('option',{name:'10',exact:true}).click();return true;");self.settle();self.assertEqual(len(self.metrics()['rows']),10) def test_14_false_contact_filter_keeps_valid_requested_page(self): self.resize(1440,900) for value in ['false','invalid']: self.navigate('people',{'q':'AUDIT-PERSON','contactsOnly':value,'sort':'name','direction':'desc','page':'3'});self.code("await page.waitForFunction(()=>!new URL(location.href).searchParams.has('contactsOnly'));return true;");self.settle();current=self.metrics();self.assertEqual(current['page'],'3');self.assertEqual(current['sort'],'descending');self.assertEqual(len(current['rows']),10);self.assertEqual(current['contacts'],['false']);self.record('people-normalized-'+value,current) self.code("await page.getByRole('checkbox',{name:'Solo contactos',exact:true}).check();return true;");self.settle();self.assertEqual(self.metrics()['page'],'1');self.assertEqual(self.metrics()['contacts'],['true']) if __name__=='__main__':unittest.main(verbosity=2)