Files
fspm-web/tests/02-responsivo.spec.ts
Uriel JarethandClaude Opus 5 31ee899987 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]>
2026-08-28 02:37:42 -06:00

156 lines
6.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test, type Page } from "@playwright/test";
import { ANCHOS, RUTAS_MUESTRA } from "./rutas";
/**
* Responsividad. Corre en los cuatro proyectos de la configuración, incluidos
* los dos de WebKit: Safari e iOS son donde de verdad se rompe el diseño, y el
* cliente lo pidió por nombre.
*
* La falla que perseguimos es el desbordamiento horizontal del documento. Una
* tabla que se desplaza dentro de su contenedor está bien; el cuerpo de la
* página desplazándose en horizontal no lo está nunca.
*/
/**
* El barrido de anchos corre en Chromium y en WebKit de escritorio, no en los
* cuatro proyectos. El desborde horizontal lo produce el CSS, no el motor, así
* que repetir 5 anchos × 10 rutas en cuatro navegadores multiplicaba el tiempo
* de la suite sin agregar información. WebKit sigue incluido porque es donde
* aparecen las diferencias reales de `dvh`, `env()` y flexbox, y los proyectos
* móviles conservan las pruebas de interacción de más abajo.
*/
test.describe.configure({ mode: "parallel" });
/**
* 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 {
desborda: movio > 2,
ancho: document.body.scrollWidth,
visible: doc.clientWidth,
movio,
};
});
}
/** Devuelve los elementos que sobresalen del ancho de la ventana, para poder señalarlos. */
async function culpablesDeDesborde(page: Page): Promise<string[]> {
return page.evaluate(() => {
const limite = document.documentElement.clientWidth;
const malos: string[] = [];
for (const el of Array.from(document.querySelectorAll<HTMLElement>("body *"))) {
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) continue;
// 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;
while (padre && padre !== document.body) {
const estilo = getComputedStyle(padre);
if (estilo.overflowX === "auto" || estilo.overflowX === "scroll" || estilo.overflowX === "hidden") {
contenido = true;
break;
}
padre = padre.parentElement;
}
if (contenido) continue;
const clases = typeof el.className === "string" ? el.className.slice(0, 60) : "";
malos.push(`${el.tagName.toLowerCase()}${clases ? "." + clases.trim().split(/\s+/).join(".") : ""}`);
if (malos.length >= 6) break;
}
}
return malos;
});
}
for (const ruta of RUTAS_MUESTRA) {
test(`sin desborde horizontal en ${ruta}`, async ({ page }, info) => {
test.skip(
!["escritorio-chromium", "escritorio-webkit"].includes(info.project.name),
"El barrido de anchos corre solo en los dos proyectos de escritorio; ver la nota de arriba.",
);
await page.goto(ruta, { waitUntil: "domcontentloaded" });
for (const ancho of ANCHOS) {
await page.setViewportSize({ width: ancho, height: 900 });
// dar chance al reflow y a las imágenes
await page.waitForTimeout(220);
const r = await desbordaEnHorizontal(page);
if (r.desborda) {
const culpables = await culpablesDeDesborde(page);
expect(
r.desborda,
`${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);
}
}
});
}
test("el menú móvil abre, es navegable y cierra", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 780 });
await page.goto("/", { waitUntil: "domcontentloaded" });
const disparador = page.getByRole("button", { name: /abrir men[úu]/i });
await expect(disparador).toBeVisible();
await disparador.click();
const menu = page.locator("#menu-movil");
await expect(menu).toBeVisible();
await expect(menu.getByRole("link").first()).toBeVisible();
await page.getByRole("button", { name: /cerrar men[úu]/i }).click();
await expect(menu).toBeHidden();
});
test("el widget de WhatsApp no tapa contenido ni desborda en móvil", async ({ page }) => {
await page.setViewportSize({ width: 320, height: 640 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /contactar por whatsapp/i }).click();
const panel = page.locator("#panel-whatsapp");
await expect(panel).toBeVisible();
const caja = await panel.boundingBox();
expect(caja, "el panel debe tener caja").not.toBeNull();
expect(caja!.x, "el panel se sale por la izquierda").toBeGreaterThanOrEqual(-1);
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 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: "domcontentloaded" });
const r = await desbordaEnHorizontal(page);
expect(r.desborda, "el comparador debe desplazarse dentro de su contenedor, no mover la página").toBe(false);
});