Files
fspm-web/tests/02-responsivo.spec.ts
T
Uriel JarethandClaude Opus 5 0e0331c1fc feat: catálogo como centro del embudo y captura de baja fricción
Reestructuración pedida por el cliente tras revisar la primera versión.

Conversión:
- Formulario de 14 campos a 4: nombre, correo, teléfono y mensaje. La
  clasificación de riesgo ya no se pregunta, se deduce del contexto de la
  página y viaja oculta. Menos fricción, misma riqueza para el análisis.
- El inicio deja de bifurcar y empuja al catálogo: 36 llamados desembocan
  ahí, y las rutas por industria y escenario pasan a ser la forma de entrar
  al catálogo ya filtrado, en lugar de competir como destino.
- Catálogo rehecho como tienda: barra lateral con filtros, búsqueda que
  acepta expresiones regulares, títulos que abren la ficha, un solo botón
  primario por tarjeta y el comparador detrás de un desplegable.
- Ficha de producto orientada al cierre: bloque en lenguaje llano sobre si
  el equipo corresponde al caso, y "Cotizar este equipo" con el formulario
  precargado. Se retiraron los enlaces que sacaban del embudo a leer normas.
- Todos los llamados se unifican en "Recibir cotización".
- Widget de WhatsApp: nombre, teléfono, correo y mensaje libre. El texto que
  escribe la persona es el que viaja a la conversación.

Doble audiencia:
- 13 escenarios con la iconografía original de FSPM traducen un lugar
  reconocible a clases de fuego y productos, para quien no domina la
  terminología. La capa técnica se conserva para búsqueda.
- Héroe con fotografía en todas las landings.

Correcciones:
- El contenedor moría al arrancar porque el CLI de Prisma no podía escribir
  sus motores; el chown ahora incluye /app/prisma-cli.
- Iconos repetidos entre escenarios. El set original no tiene icono de
  cocina, así que ese escenario usa un marcador tipográfico explícito.
- FIREMIKS no aparecía en ningún escenario.
- Los ids de campo del formulario colisionaban con dos instancias por página.
- Se documentó el despliegue, que faltaba por completo.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-28 01:30:44 -06:00

133 lines
5.3 KiB
TypeScript
Raw 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" });
async function desbordaEnHorizontal(page: Page): Promise<{ desborda: boolean; ancho: number; visible: number }> {
return page.evaluate(() => {
const doc = document.documentElement;
return {
// 2px de tolerancia por redondeo de subpíxel
desborda: doc.scrollWidth > doc.clientWidth + 2,
ancho: doc.scrollWidth,
visible: doc.clientWidth,
};
});
}
/** 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;
if (r.right > limite + 2 || r.left < -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: "networkidle" });
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} desborda a ${ancho}px (documento ${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 provocar desborde del documento").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" });
const r = await desbordaEnHorizontal(page);
expect(r.desborda, "el comparador no debe desplazar el cuerpo de la página").toBe(false);
});