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

202 lines
13 KiB
Python
Raw Normal View History

2026-10-24 23:19:17 +00:00
#!/usr/bin/env python3
"""L03.2: búsquedas locales y regresión L03.1 mediante el MCP existente.
Reutiliza una sesión sintética activa y el volumen de visual-audit-volume.py.
No inicia servicios ni escribe entidades. Las variantes adicionales son respuestas GET.
"""
import json
import os
from pathlib import Path
import runpy
import unittest
from urllib.parse import quote, parse_qs, urlsplit
ROOT = Path(__file__).resolve().parent.parent
CONTEXT = runpy.run_path(str(ROOT / "scripts/list-context-ui.test.py"))
class SearchUiTests(CONTEXT["ListContextUiTests"]):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.output = ROOT / ".artifacts/playwright/l03-2/after"
cls.output.mkdir(parents=True, exist_ok=True)
cls.measurements = cls.output / ("measurements-" + str(os.getpid()) + ".json")
def query(self, value):
self.code("await page.locator('main input[data-quick-search]').fill(" + json.dumps(value) + ");return true;")
self.settle()
return self.search_metrics()
def search_metrics(self):
return self.code(r"""return page.locator('main table').first().evaluate(table=>{
const root=table.closest('[data-slot=entity-table]'),input=root.querySelector('input[data-quick-search]');
return {url:location.pathname+location.search,query:input.value,focusSearch:document.activeElement===input,
count:Number(root.textContent.match(/(?:de|of) (\d+) /)?.[1]??0),
sort:table.querySelector('th[aria-sort]')?.getAttribute('aria-sort'),page:root.querySelector('[aria-current=page]')?.textContent,
rows:[...table.querySelectorAll('tbody tr[data-entity-id]')].map(row=>({id:row.dataset.entityId,text:row.textContent})),
overflow:document.documentElement.scrollWidth>innerWidth};});""")
def assert_query(self, query, count=None):
value = self.query(query)
self.assertEqual(value["query"], query)
self.assertEqual(parse_qs(urlsplit(value["url"]).query).get("q"), [query])
self.assertTrue(value["focusSearch"])
if count is not None:
self.assertEqual(value["count"], count)
self.record("search-" + query, value)
return value
def company_fixture(self):
self.code('page.searchCompanyUrl=' + json.dumps(self.origin + '/api/companies/' + self.company) + """;
await page.route(page.searchCompanyUrl,async route=>{
const response=await route.fetch(),data=await response.json(),company=data.company??data;
company.people.slice(0,3).forEach((person,index)=>{person.name=['Álvaro Pena','Álvaro Peña','A\\u0301lvaro Pen\\u0303a'][index]});
company.departments=Array.from({length:50},(_,index)=>({id:crypto.randomUUID(),companyId:company.id,
name:(index%2?'Administración Peña':'Administracion Pena')+' PC-'+String(index).padStart(3,'0'),active:true}));
await route.fulfill({response,json:data});
});return true;""")
def test_15_asset_search_matrix_and_return(self):
for width, height in CONTEXT["SIZES"]:
self.resize(width, height)
self.navigate()
history_length = self.code("return page.evaluate(()=>history.length);")
first = self.assert_query("portatil", 41)
for query in [" PORTÁTIL ", "porta\u0301til", "rtatil"]:
result = self.assert_query(query, 41)
self.assertEqual(result["rows"], first["rows"])
self.assertFalse(result["overflow"])
self.assertEqual(self.code("return page.evaluate(()=>history.length);"), history_length)
self.query(" portatil ")
self.code("await page.getByRole('tab',{name:'Portátil',exact:true}).click();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.open_asset()
self.code("await page.goBack();return true;")
self.ready()
self.wait_return_focus(before["focusRow"])
after = self.metrics()
for key in ["rows", "search", "sort", "page", "x", "y", "focusRow"]:
self.assertEqual(before[key], after[key], str(width) + key)
self.record("accent-return-" + str(width), after)
if width in [1920, 390, 320]:
self.capture("asset-search-return-" + str(width))
def test_16_resource_search_context(self):
self.resize(390, 844)
self.navigate("resources")
first = self.assert_query("aplicacion")
self.assertGreaterEqual(first["count"], 50)
for query in [" APLICACIÓN ", "aplicacio\u0301n"]:
value = self.assert_query(query, first["count"])
self.assertEqual(value["rows"], first["rows"])
self.capture("resource-trimmed-search-390")
self.code("await page.locator('main tbody tr a').first().click();await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');return true;")
self.ready("resources")
self.assertEqual(self.search_metrics()["query"], "aplicacio\u0301n")
def test_17_people_unicode_and_name_scope(self):
self.company_fixture()
try:
self.resize(390, 844)
self.navigate("people")
result = self.assert_query("alvaro")
self.assertGreaterEqual(result["count"], 3)
pena = self.assert_query("Pena", 1)
self.assertTrue(all("Peña" not in row["text"] for row in pena["rows"]))
enye = self.assert_query("PEN\u0303A")
self.assertGreaterEqual(enye["count"], 2)
self.assertTrue(all(" Pena" not in row["text"] for row in enye["rows"]))
self.capture("people-enye-390")
self.assert_query("person01@audit.example.test", 0)
finally:
self.code("await page.unroute(page.searchCompanyUrl);return true;")
def test_18_catalog_names(self):
self.resize(1440, 900)
for path, query, label in [("/catalogs/assets", " PORTATIL ", "Portátil"), ("/catalogs/resources", "aplicacion", "Aplicación")]:
self.code("await page.goto(" + json.dumps(self.origin + path) + ");await page.locator('main tbody tr[data-entity-id]').first().waitFor();return true;")
value = self.assert_query(query, 1)
self.assertIn(label, value["rows"][0]["text"])
self.capture("catalog-" + path.split("/")[-1])
def test_19_generic_table_and_technical_text(self):
self.company_fixture()
try:
self.resize(1024, 768)
self.navigate("departments")
self.assertEqual(self.query("ADMINISTRACION")["count"], 50)
self.assertEqual(self.query("Pena")["count"], 25)
self.assertEqual(self.query("Peña")["count"], 25)
self.assertEqual(self.query("PC-001")["count"], 1)
self.assertEqual(self.query("PC001")["count"], 0)
self.query("administracio\u0301n")
self.capture("generic-departments-1024")
finally:
self.code("await page.unroute(page.searchCompanyUrl);return true;")
def test_20_local_pickers_keep_provisional_selection(self):
self.resize(320, 640)
module = "/@fs/" + quote(str(ROOT / "packages/rdx-ui/src/picker-browser-fixture.tsx"), safe="/")
for kind in ["fields", "options"]:
self.navigate()
self.code("await page.evaluate(async module=>{const {mountPickerFixture}=await import(module);const target=document.createElement('div');document.body.append(target);window.searchTestRoot=mountPickerFixture(target,{count:50,locale:'es',longNames:true,kind:" + json.dumps(kind) + "})}," + json.dumps(module) + ");await page.getByRole('button',{name:'Seleccionar campos',exact:true}).click();await page.getByRole('dialog').locator('input[data-quick-search]').waitFor();return true;")
self.assertTrue(self.code("return page.getByRole('dialog').locator('input[data-quick-search]').evaluate(e=>e===document.activeElement);"))
self.code("await page.getByRole('dialog').locator('input[data-quick-search]').fill(' DESCRIPCION TECNICA ');return true;")
self.settle()
selector = "checkbox" if kind == "fields" else "radio"
self.code("await page.getByRole('dialog').locator('tbody').getByRole(" + json.dumps(selector) + ").first().click();await page.getByRole('dialog').locator('input[data-quick-search]').fill('no-matches-xyz');return true;")
self.settle()
self.assertEqual(self.code("return page.getByRole('dialog').locator('tbody tr[data-entity-id]').count();"), 0)
self.code("await page.getByRole('dialog').locator('input[data-quick-search]').fill('descripcion tecnica');return true;")
self.settle()
self.assertEqual(self.code("return page.getByRole('dialog').locator('tbody [aria-checked=true]').count();"), 1)
self.capture("picker-" + kind + "-320")
self.code("await page.getByRole('button',{name:'Seleccionar',exact:true}).click();await page.getByRole('dialog').waitFor({state:'hidden'});return true;")
self.assertIn("option-0", self.code("return page.getByLabel('Resultado',{exact:true}).textContent();"))
self.code("await page.evaluate(()=>window.searchTestRoot.unmount());return true;")
def test_21_query_during_refetch_and_zero_results(self):
self.resize(1440, 900)
self.navigate()
self.code('page.searchUrl=' + json.dumps(self.origin + '/api/assets/' + self.company) + ";page.searchGates=[];page.searchStarted=new Promise(resolve=>page.searchStart=resolve);await page.route(page.searchUrl,async route=>{page.searchStart();await new Promise(resolve=>page.searchGates.push(resolve));await route.continue()});await page.evaluate(()=>{window.dispatchEvent(new Event('offline'));window.dispatchEvent(new Event('online'))});await page.searchStarted;await page.locator('[data-query-state=refreshing]').waitFor();await page.evaluate(()=>window.searchTable=document.querySelector('main table'));return true;")
try:
self.assert_query(" portatil ", 41)
self.code("const response=page.waitForResponse(page.searchUrl);for(const release of page.searchGates)release();await response;return true;")
self.ready()
self.assertTrue(self.code("return page.evaluate(()=>window.searchTable===document.querySelector('main table')&&document.activeElement.hasAttribute('data-quick-search'));"))
self.assertEqual(self.search_metrics()["count"], 41)
self.assert_query("no-results-xyz", 0)
self.capture("zero-results-1440")
self.code("await page.getByRole('button',{name:'Limpiar filtros',exact:true}).click();return true;")
self.assertEqual(self.search_metrics()["query"], "")
finally:
self.code("for(const release of page.searchGates)release();await page.unroute(page.searchUrl);return true;")
def test_22_language_and_shared_query(self):
self.resize(390, 844)
self.navigate(params={"q": " PORTATIL ", "sort": "name", "direction": "desc", "page": "3"})
before = self.search_metrics()
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;")
self.settle()
after = self.search_metrics()
for key in ["query", "count", "sort", "page", "url"]:
self.assertEqual(before[key], after[key])
self.assertEqual(after["count"], 41)
self.record("english-original-query", after)
self.capture("english-search-390")
self.code("await page.reload();await page.locator('[data-query-state=ready]').waitFor();return true;")
self.assertEqual(self.search_metrics()["query"], " PORTATIL ")
finally:
# La aplicación vuelve al idioma inicial tras recargar; solo restaurar si sigue en inglés.
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 load_tests(loader, tests, pattern):
return loader.loadTestsFromTestCase(SearchUiTests)
if __name__ == "__main__":
unittest.main(verbosity=2)