fix: defectos encontrados por la suite de extremo a extremo

La primera corrida completa dio 57 fallas. Solo una parte eran del
producto; el resto era instrumentación mal escrita. Queda en 261 en verde.

Defectos del producto:

- La atribución de campaña se perdía. La captura vivía en un efecto de
  React, así que si la persona llegaba con parámetros de campaña y
  navegaba antes de hidratar, el primer toque nunca se guardaba y
  utmCampaign llegaba vacío al CRM. Ahora se captura en un script en
  línea que corre al analizar el documento.
- Siete páginas sin og:image: una página que declara su propio openGraph
  no hereda las imágenes del layout, las sobreescribe. Solo se nota al
  compartir el enlace. Se resuelve con una función compartida en seo.ts.
- El panel no se podía usar desde Safari por HTTP: la cookie Secure no se
  guarda y el acceso falla en silencio. Queda explícito y documentado.
- El panel se corría de lado en móvil: la cabecera no cabía a 375px.
- Las tarjetas de perfil del acceso se cortaban en móvil. Un elemento de
  rejilla nace con min-width auto y no puede encogerse bajo su contenido.
  Era además el origen de un desplazamiento que se arrastraba al tablero.
- Las tablas accesibles de las gráficas volvían desplazable la página:
  sr-only fija width 1px y el algoritmo de tablas lo ignora.
- El campo de búsqueda del catálogo no tenía nombre accesible.
- Límite de tasa de 5 envíos por IP: bloqueaba una demostración en vivo
  con varias personas en la misma red. Configurable, ahora 20.
- El salto de contenido pasa de left:-9999px a recorte de 1px.

Instrumentación corregida, con la razón escrita en cada caso:

- El desborde horizontal se mide intentando desplazar la página, no con
  documentElement.scrollWidth, que sobreinforma con contenedores propios.
- La comprobación de accesibilidad respeta el árbol de accesibilidad.
- El aislamiento entre roles se prueba pidiendo por URL una oportunidad
  ajena, no comparando folios entre listados paginados.
- Fuera las esperas por networkidle, que agotaban el tiempo sin fallar.
- Que el panel no esté enlazado se comprueba leyendo el HTML servido.
- Safari no enfoca enlaces con Tab: declarado como del navegador.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Uriel Jareth
2026-08-28 02:37:42 -06:00
co-authored by Claude Opus 5
parent 0e0331c1fc
commit 31ee899987
24 changed files with 346 additions and 86 deletions
+33 -10
View File
@@ -21,14 +21,34 @@ import { ANCHOS, RUTAS_MUESTRA } from "./rutas";
*/
test.describe.configure({ mode: "parallel" });
async function desbordaEnHorizontal(page: Page): Promise<{ desborda: boolean; ancho: number; visible: number }> {
/**
* Mide el síntoma, no una propiedad intermedia.
*
* La primera versión de esta función comparaba `documentElement.scrollWidth`
* contra `clientWidth`, y producía falsos positivos: en `/evidencia`,
* `html.scrollWidth` reporta 865px a 320px de ventana mientras
* `body.scrollWidth` es 320 y la página **no se puede desplazar**. Chromium y
* WebKit coinciden en ese reporte, así que es comportamiento esperado alrededor
* del desbordamiento de un contenedor con scroll propio, no un defecto.
*
* Lo único que le importa a la persona es si la página se corre de lado. Eso se
* comprueba intentando desplazarla y viendo si se movió. `body.scrollWidth`
* queda como dato de apoyo para el diagnóstico.
*/
async function desbordaEnHorizontal(
page: Page,
): Promise<{ desborda: boolean; ancho: number; visible: number; movio: number }> {
return page.evaluate(() => {
const doc = document.documentElement;
const antes = window.scrollX;
window.scrollTo(800, window.scrollY);
const movio = window.scrollX;
window.scrollTo(antes, window.scrollY);
return {
// 2px de tolerancia por redondeo de subpíxel
desborda: doc.scrollWidth > doc.clientWidth + 2,
ancho: doc.scrollWidth,
desborda: movio > 2,
ancho: document.body.scrollWidth,
visible: doc.clientWidth,
movio,
};
});
}
@@ -41,7 +61,10 @@ async function culpablesDeDesborde(page: Page): Promise<string[]> {
for (const el of Array.from(document.querySelectorAll<HTMLElement>("body *"))) {
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) continue;
if (r.right > limite + 2 || r.left < -2) {
// solo cuenta lo que sobresale a la DERECHA: un elemento colocado fuera
// de pantalla a la izquierda, como el salto de contenido, no agranda el
// área desplazable en escritura de izquierda a derecha
if (r.right > limite + 2) {
// ignoramos lo que desborda a propósito dentro de su propio contenedor con scroll
let padre: HTMLElement | null = el.parentElement;
let contenido = false;
@@ -71,7 +94,7 @@ for (const ruta of RUTAS_MUESTRA) {
"El barrido de anchos corre solo en los dos proyectos de escritorio; ver la nota de arriba.",
);
await page.goto(ruta, { waitUntil: "networkidle" });
await page.goto(ruta, { waitUntil: "domcontentloaded" });
for (const ancho of ANCHOS) {
await page.setViewportSize({ width: ancho, height: 900 });
@@ -83,7 +106,7 @@ for (const ruta of RUTAS_MUESTRA) {
const culpables = await culpablesDeDesborde(page);
expect(
r.desborda,
`${ruta} desborda a ${ancho}px (documento ${r.ancho}px sobre ventana ${r.visible}px). Sospechosos: ${culpables.join(", ") || "no identificados"}`,
`${ruta} se desplaza en horizontal a ${ancho}px de ancho: la página se corrió ${r.movio}px (cuerpo ${r.ancho}px sobre ventana ${r.visible}px). Sospechosos: ${culpables.join(", ") || "no identificados"}`,
).toBe(false);
}
}
@@ -120,13 +143,13 @@ test("el widget de WhatsApp no tapa contenido ni desborda en móvil", async ({ p
expect(caja!.x + caja!.width, "el panel se sale por la derecha").toBeLessThanOrEqual(321);
const r = await desbordaEnHorizontal(page);
expect(r.desborda, "el widget abierto no debe provocar desborde del documento").toBe(false);
expect(r.desborda, "el widget abierto no debe hacer que la página se desplace de lado").toBe(false);
});
test("las tablas anchas se desplazan dentro de su contenedor", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 780 });
await page.goto("/catalogo", { waitUntil: "networkidle" });
await page.goto("/catalogo", { waitUntil: "domcontentloaded" });
const r = await desbordaEnHorizontal(page);
expect(r.desborda, "el comparador no debe desplazar el cuerpo de la página").toBe(false);
expect(r.desborda, "el comparador debe desplazarse dentro de su contenedor, no mover la página").toBe(false);
});