metis-desk/scripts/list-closure-ui.test.py

103 lines
11 KiB
Python
Raw Normal View History

2026-10-24 23:19:17 +00:00
#!/usr/bin/env python3
"""L04.4: regresión de cierre por MCP con lecturas sintéticas; no guarda formularios."""
import json
import os
from pathlib import Path
import runpy
import unittest
from urllib.parse import quote
ROOT = Path(__file__).resolve().parent.parent
EMBEDDED = runpy.run_path(str(ROOT / 'scripts/embedded-relations-ui.test.py'))
class ListClosureUiTests(EMBEDDED['EmbeddedRelationsUiTests']):
artifact_directory = ROOT / '.artifacts/playwright/l04-4/closure'
def theme(self, dark):
self.code('await page.emulateMedia({colorScheme:'+json.dumps('dark' if dark else 'light')+'});await page.evaluate(dark=>document.documentElement.classList.toggle("dark",dark),'+json.dumps(dark)+');return true;')
self.settle()
def geometry(self, label):
value=self.code("""return page.locator('[role=dialog]').last().or(page.locator('main')).last().evaluate(root=>{
const rect=e=>{const r=e.getBoundingClientRect();return{x:r.x,y:r.y,width:r.width,height:r.height,bottom:r.bottom,right:r.right}},table=root.querySelector('table'),body=table?.querySelector('tbody'),container=table?.closest('[data-slot=table-container]'),focus=document.activeElement,r=focus?.getBoundingClientRect();
return{url:location.pathname+location.search,dark:document.documentElement.classList.contains('dark'),viewport:{width:innerWidth,height:innerHeight},container:rect(root),globalOverflow:document.documentElement.scrollWidth>innerWidth,table:table?rect(table):null,internalOverflow:container?container.scrollWidth>container.clientWidth:false,beforeData:body?.getBoundingClientRect().top,visibleRows:body?[...body.rows].filter(row=>{const a=row.getBoundingClientRect();return a.top>=0&&a.bottom<=innerHeight}).length:0,headers:table?[...table.querySelectorAll('th')].map(th=>({name:th.textContent,scope:th.scope,sort:th.getAttribute('aria-sort')})):[],focus:{tag:focus?.tagName,name:focus?.getAttribute('aria-label'),rect:focus?rect(focus):null,inViewport:r?r.top>=0&&r.bottom<=innerHeight&&r.left>=0&&r.right<=innerWidth:false},colors:{text:getComputedStyle(root).color,background:getComputedStyle(root).backgroundColor}};
});""")
self.record(label,value)
self.assertFalse(value['globalOverflow'],label)
return value
def test_94_light_dark_geometry(self):
for dark in [False,True]:
for width,height in EMBEDDED['CONTEXT_SIZES']:
self.resize(width,height)
for section in ['assets','resources','mail-configurations']:
self.list(section);self.theme(dark)
self.code("await page.locator('main input[data-quick-search]').focus();return true;")
self.settle();value=self.geometry(f'{section}-{width}-'+str(dark))
self.assertTrue(value['focus']['inViewport'])
if width in [1440,390,320]:self.capture(f'{section}-{width}-'+('dark' if dark else 'light'))
self.theme(False)
def test_95_dashboard_query_contract(self):
self.resize(390,844)
self.code("page.closureDashboard={mode:'hold',release:[]};await page.route(page.adoptionOrigin+'/api/dashboard/details?*',async route=>{const f=page.closureDashboard;if(f.mode==='hold')await new Promise(resolve=>f.release.push(resolve));if(['error','denied'].includes(f.mode))return route.fulfill({status:f.mode==='denied'?403:503,json:{code:f.mode==='denied'?'FORBIDDEN':'INTERNAL_ERROR'}});return route.fulfill({json:{items:page.adoption.inventory.assets.slice(0,10).map(a=>({id:a.id,companyId:page.adoptionCompany,companyName:page.adoption.detail.company.name,name:a.values.name,kind:'asset',recordedAt:null,actorName:null})),total:50}})});return true;")
self.code('await page.adoptionGoto('+json.dumps(self.origin+self.base)+");await page.getByRole('button').filter({hasText:'Activos registrados'}).first().click();await page.getByRole('dialog').waitFor();return true;")
self.record('dashboard-initial-pending',self.code("return {table:await page.getByRole('dialog').getByRole('table').count(),status:await page.getByRole('dialog').getByRole('status').allTextContents()};"));self.capture('dashboard-initial-390')
self.assertEqual(self.code("return page.getByRole('dialog').getByRole('table').count();"),1)
initial=self.code("return page.getByRole('dialog').boundingBox();")
self.code("page.closureDashboard.mode='ready';for(const release of page.closureDashboard.release.splice(0))release();await page.getByRole('dialog').locator('tbody tr').first().waitFor();return true;")
self.settle();loaded=self.code("return page.getByRole('dialog').boundingBox();");self.record('dashboard-height',{'initial':initial,'loaded':loaded});self.assertEqual(initial,loaded)
self.code("page.closureDashboard.mode='error';await page.evaluate(()=>{window.dispatchEvent(new Event('offline'));window.dispatchEvent(new Event('online'))});await page.getByRole('dialog').getByRole('alert').waitFor();return true;")
current=self.code("return {table:await page.getByRole('dialog').getByRole('table').count(),rows:await page.getByRole('dialog').locator('tbody tr').count(),retry:await page.getByRole('dialog').getByRole('button',{name:'Reintentar',exact:true}).count()};")
self.record('dashboard-stale',current);self.capture('dashboard-stale-390');self.assertEqual(current,{'table':1,'rows':10,'retry':1})
self.code("page.closureDashboard.mode='ready';const response=page.waitForResponse(r=>r.url().includes('/api/dashboard/details?')&&r.status()===200);await page.getByRole('dialog').getByRole('button',{name:'Reintentar',exact:true}).click();await response;await page.waitForFunction(()=>document.querySelector('[role=dialog] table')?.getAttribute('aria-busy')!=='true');page.closureDashboard.mode='denied';await page.evaluate(()=>{window.dispatchEvent(new Event('offline'));window.dispatchEvent(new Event('online'))});await page.getByRole('dialog').getByText('No tienes permiso para consultar esta información.',{exact:true}).waitFor();return true;")
denied=self.code("return {rows:await page.getByRole('dialog').locator('tbody tr').count(),retry:await page.getByRole('dialog').getByRole('button',{name:'Reintentar',exact:true}).count()};");self.record('dashboard-denied',denied);self.assertEqual(denied,{'rows':0,'retry':0});self.capture('dashboard-denied-390');self.code("await page.keyboard.press('Escape');return true;")
def test_96_remote_picker_both_themes(self):
for dark in [False,True]:
for width,height in [(1440,900),(390,844),(320,640)]:
self.resize(width,height);self.open_publication_picker();self.theme(dark)
self.code("const d=page.getByRole('dialog').last();await d.locator('input[data-quick-search]').focus();await page.keyboard.press('Tab');await page.keyboard.press('Shift+Tab');return true;")
value=self.geometry('picker-'+str(width)+'-'+str(dark));self.assertTrue(value['focus']['inViewport']);self.capture('picker-'+str(width)+'-'+('dark' if dark else 'light'))
self.code("await page.keyboard.press('Escape');await page.keyboard.press('Escape');return true;")
self.theme(False)
def test_97_selected_links_contrast_both_themes(self):
self.resize(1920,1080)
for section,name in [('assets','AUDIT-PORT-01'),('resources','AUDIT-APP-01')]:
for dark in [False,True]:
self.list(section,{'q':name});self.theme(dark)
self.code("await page.getByRole('row').filter({has:page.getByRole('link',{name:"+json.dumps(name)+",exact:true})}).locator('td').nth(2).click();return true;")
self.settle()
value=self.code("""return page.getByRole('row').filter({has:page.getByRole('link',{name:"""+json.dumps(name)+""",exact:true})}).evaluate(row=>{
const rgb=c=>{const ctx=document.createElement('canvas').getContext('2d');ctx.fillStyle=c;ctx.fillRect(0,0,1,1);return [...ctx.getImageData(0,0,1,1).data].slice(0,3)};
const bg=e=>{const colors=[];for(let n=e;n;n=n.parentElement)colors.unshift(getComputedStyle(n).backgroundColor);const ctx=document.createElement('canvas').getContext('2d');ctx.fillStyle='#fff';ctx.fillRect(0,0,1,1);for(const color of colors){ctx.fillStyle=color;ctx.fillRect(0,0,1,1)}return [...ctx.getImageData(0,0,1,1).data].slice(0,3)};
const lum=c=>c.map(v=>v/255).map(v=>v<=.04045?v/12.92:((v+.055)/1.055)**2.4).reduce((s,v,i)=>s+v*[.2126,.7152,.0722][i],0);
return [...row.querySelectorAll('a,button,[class*=text-muted-foreground]')].filter(e=>e.getClientRects().length).map(e=>{const fg=rgb(getComputedStyle(e).color),background=bg(e),l=[lum(fg),lum(background)].sort((a,b)=>a-b);return{name:e.textContent.trim()||e.getAttribute('aria-label'),foreground:fg,background,ratio:(l[1]+.05)/(l[0]+.05)}});
});""")
self.record(section+'-selected-contrast-'+str(dark),value);self.capture(section+'-selected-contrast-'+('dark'if dark else'light'));self.assertTrue(all(v['ratio']>=4.5 for v in value),value)
self.theme(False)
def test_98_dashboard_other_variants(self):
self.resize(390,844)
self.code("page.closureCompanyMode='hold';page.closureCompanyRelease=[];await page.route(page.adoptionOrigin+'/api/dashboard/companies?*',async route=>{if(page.closureCompanyMode==='hold')await new Promise(resolve=>page.closureCompanyRelease.push(resolve));return route.fulfill({json:{items:page.adoption.companies.companies.map(c=>({id:c.id,name:c.name,companies:1,people:50,assets:50,resources:50})),total:15}})});return true;")
self.code('await page.adoptionGoto('+json.dumps(self.origin+'/dashboard')+");await page.getByRole('region',{name:'Resumen por compañías',exact:true}).getByRole('button',{name:'Ver todos',exact:true}).click();await page.getByRole('dialog').locator('input[data-quick-search]').waitFor();return true;")
self.assertEqual(self.code("return page.getByRole('dialog').getByRole('table').count();"),1)
self.code("page.closureCompanyMode='ready';for(const release of page.closureCompanyRelease.splice(0))release();await page.getByRole('dialog').locator('tbody tr[data-entity-id]').first().waitFor();await page.getByRole('dialog').locator('input[data-quick-search]').fill('zz-no-results');return true;")
self.assertEqual(self.code("return page.getByRole('dialog').locator('tbody tr[data-entity-id]').count();"),0);self.capture('dashboard-companies-no-results-390')
self.code("await page.keyboard.press('Escape');return true;")
self.code("await page.getByRole('region',{name:'Actividad reciente',exact:true}).getByRole('button',{name:'Ver todos',exact:true}).click();await page.getByRole('dialog').locator('tbody tr[data-entity-id]').first().waitFor();return true;")
headers=self.code("return page.getByRole('dialog').getByRole('columnheader').allTextContents();");self.assertEqual(len(headers),4);self.record('dashboard-activity-headers',headers);self.capture('dashboard-activity-390');self.code("await page.keyboard.press('Escape');return true;")
def load_tests(loader, tests, pattern):
return unittest.TestSuite(ListClosureUiTests(name) for name in sorted(ListClosureUiTests.__dict__) if name.startswith('test_'))
if __name__ == '__main__':
unittest.main()