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]>
156 lines
6.4 KiB
TypeScript
156 lines
6.4 KiB
TypeScript
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);
|
||
});
|