202 lines
13 KiB
Python
202 lines
13 KiB
Python
|
|
#!/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)
|