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]>
105 lines
4.2 KiB
TypeScript
105 lines
4.2 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { RUTAS_MUESTRA, TODAS_LAS_RUTAS } from "./rutas";
|
|
|
|
/**
|
|
* Humo: cada ruta pública responde, tiene una sola cabecera principal, título
|
|
* propio y los metadatos que la auditoría de marca señaló como ausentes en el
|
|
* sitio original.
|
|
*/
|
|
|
|
/**
|
|
* El barrido de las 45 rutas corre en un solo proyecto. Que una ruta responda
|
|
* 200 y traiga un `<h1>` lo decide el servidor, no el motor del navegador, así
|
|
* que repetirlo cuatro veces solo alargaba la suite. Lo que sí es sensible al
|
|
* motor —responsividad, interacción, foco— vive en las otras suites y ahí sí
|
|
* corre en WebKit.
|
|
*/
|
|
test.describe("rutas públicas responden", () => {
|
|
for (const ruta of TODAS_LAS_RUTAS) {
|
|
test(`200 y contenido en ${ruta}`, async ({ page }, info) => {
|
|
test.skip(
|
|
info.project.name !== "escritorio-chromium",
|
|
"El barrido completo de rutas corre en un solo proyecto; ver la nota de arriba.",
|
|
);
|
|
|
|
const res = await page.goto(ruta, { waitUntil: "domcontentloaded" });
|
|
expect(res?.status(), `${ruta} debe responder 200`).toBe(200);
|
|
|
|
// exactamente un h1: si hay dos, la jerarquía del documento está mal
|
|
const h1 = page.locator("h1");
|
|
await expect(h1, `${ruta} debe tener un solo h1`).toHaveCount(1);
|
|
await expect(h1).not.toBeEmpty();
|
|
|
|
const titulo = await page.title();
|
|
expect(titulo.length, `${ruta} necesita un <title> con sustancia`).toBeGreaterThan(12);
|
|
});
|
|
}
|
|
});
|
|
|
|
test.describe("metadatos que faltaban en el sitio original", () => {
|
|
for (const ruta of RUTAS_MUESTRA) {
|
|
test(`description y open graph en ${ruta}`, async ({ page }) => {
|
|
await page.goto(ruta, { waitUntil: "domcontentloaded" });
|
|
|
|
const desc = await page.locator('meta[name="description"]').getAttribute("content");
|
|
expect(desc, `${ruta} sin meta description`).toBeTruthy();
|
|
expect(desc!.length, `${ruta}: description demasiado corta`).toBeGreaterThan(50);
|
|
expect(desc!.length, `${ruta}: description demasiado larga para el snippet`).toBeLessThan(300);
|
|
|
|
const ogTitulo = await page.locator('meta[property="og:title"]').getAttribute("content");
|
|
expect(ogTitulo, `${ruta} sin og:title`).toBeTruthy();
|
|
|
|
const ogImagen = await page.locator('meta[property="og:image"]').first().getAttribute("content");
|
|
expect(ogImagen, `${ruta} sin og:image`).toBeTruthy();
|
|
|
|
const canonica = await page.locator('link[rel="canonical"]').getAttribute("href");
|
|
expect(canonica, `${ruta} sin canónica`).toBeTruthy();
|
|
});
|
|
}
|
|
});
|
|
|
|
test("idioma declarado en español de México", async ({ page }) => {
|
|
await page.goto("/");
|
|
await expect(page.locator("html")).toHaveAttribute("lang", /^es/);
|
|
});
|
|
|
|
test("sitemap lista rutas reales y excluye el panel", async ({ page }) => {
|
|
const res = await page.goto("/sitemap.xml");
|
|
expect(res?.status()).toBe(200);
|
|
const xml = await page.content();
|
|
|
|
expect(xml).toContain("/catalogo");
|
|
expect(xml).toContain("/soluciones/");
|
|
expect(xml, "el panel no debe aparecer en el sitemap").not.toContain("/admin");
|
|
});
|
|
|
|
test("robots prohíbe el panel y la API", async ({ request }) => {
|
|
const res = await request.get("/robots.txt");
|
|
expect(res.status()).toBe(200);
|
|
const txt = await res.text();
|
|
expect(txt).toMatch(/Disallow:\s*\/admin/i);
|
|
expect(txt).toMatch(/Sitemap:/i);
|
|
});
|
|
|
|
test("iconos y tarjeta social se generan", async ({ request }) => {
|
|
for (const ruta of ["/icon", "/opengraph-image"]) {
|
|
const res = await request.get(ruta);
|
|
expect(res.status(), `${ruta} debe responder`).toBeLessThan(400);
|
|
expect(res.headers()["content-type"], `${ruta} debe ser imagen`).toContain("image");
|
|
}
|
|
});
|
|
|
|
test("sonda de salud responde", async ({ request }) => {
|
|
const res = await request.get("/api/salud");
|
|
expect(res.status()).toBe(200);
|
|
const json = (await res.json()) as { ok: boolean };
|
|
expect(json.ok).toBe(true);
|
|
});
|
|
|
|
test("una ruta inexistente devuelve 404 con salidas útiles", async ({ page }) => {
|
|
const res = await page.goto("/producto/esto-no-existe-jamas");
|
|
expect(res?.status()).toBe(404);
|
|
// un 404 sin camino de regreso es un callejón
|
|
await expect(page.getByRole("link", { name: /cat[áa]logo|soluciones|inicio|contacto/i }).first()).toBeVisible();
|
|
});
|