import { expect, test } from "@playwright/test"; import { PRODUCTOS } from "../src/data/catalogo"; /** * El circuito comercial completo, que es la razón de ser del proyecto: * * llega por una campaña → navega → envía el formulario en una ficha * → queda registrado con la campaña del primer toque y el producto exacto * * Si la atribución se pierde entre la página de aterrizaje y la de conversión, * la promesa de "medir efectividad de campañas" no se sostiene. Eso es lo que * esta prueba vigila. */ const PRODUCTO = PRODUCTOS[0]!; function sufijo(): string { return `${Date.now()}${Math.floor(Math.random() * 1000)}`; } test("formulario del inicio registra el lead y devuelve folio", async ({ page }) => { const s = sufijo(); await page.goto("/", { waitUntil: "domcontentloaded" }); const forma = page.locator("form").filter({ has: page.locator('input[name="nombre"]') }).first(); await forma.scrollIntoViewIfNeeded(); await forma.locator('input[name="nombre"]').fill(`Prueba Inicio ${s}`); await forma.locator('input[name="email"]').fill(`inicio.${s}@pruebafspm.mx`); await forma.locator('input[name="telefono"]').fill("5581000001"); await forma.locator('input[name="privacidad"]').check(); await forma.getByRole("button", { name: /solicitar/i }).click(); await expect(page.getByText(/solicitud registrada/i)).toBeVisible({ timeout: 15_000 }); await expect(page.getByText(/OPP-\d{4}-\d{4}/)).toBeVisible(); }); test("la atribución de primer toque sobrevive a la navegación", async ({ page }) => { const s = sufijo(); const campana = `e2e-primer-toque-${s}`; // 1. entra por una campaña, no directo a la ficha await page.goto(`/?utm_source=google&utm_medium=cpc&utm_campaign=${campana}&gclid=gclid-${s}`, { waitUntil: "domcontentloaded", }); // 2. navega a una ficha de producto, ya sin parámetros en la URL await page.goto(`/producto/${PRODUCTO.slug}`, { waitUntil: "domcontentloaded" }); expect(page.url()).not.toContain("utm_campaign"); // 3. convierte ahí const forma = page.locator("form").filter({ has: page.locator('input[name="nombre"]') }).first(); await forma.scrollIntoViewIfNeeded(); await forma.locator('input[name="nombre"]').fill(`Prueba Atribución ${s}`); await forma.locator('input[name="email"]').fill(`atribucion.${s}@pruebafspm.mx`); await forma.locator('input[name="telefono"]').fill("5581000002"); await forma.locator('input[name="privacidad"]').check(); // interceptamos el envío para leer lo que el navegador manda de verdad const peticion = page.waitForRequest( (r) => r.url().includes("/api/leads") && r.method() === "POST", ); await forma.getByRole("button", { name: /solicitar/i }).click(); const req = await peticion; const cuerpo = JSON.parse(req.postData() ?? "{}") as Record; expect(cuerpo.utmCampaign, "la campaña del primer toque debe viajar").toBe(campana); expect(cuerpo.utmSource).toBe("google"); expect(cuerpo.utmMedium).toBe("cpc"); expect(cuerpo.gclid).toBe(`gclid-${s}`); expect(cuerpo.productoSlug, "la ficha debe declarar su producto").toBe(PRODUCTO.slug); expect(String(cuerpo.productoUrl), "debe guardarse la URL que vio el usuario").toContain( `/producto/${PRODUCTO.slug}`, ); expect(cuerpo.canal).toBe("producto"); expect(String(cuerpo.landingPath), "la ruta de aterrizaje debe ser la de la campaña").toContain("/"); await expect(page.getByText(/solicitud registrada/i)).toBeVisible({ timeout: 15_000 }); }); test("un mismo contacto puede enviar varias veces y se acumula", async ({ page }) => { const s = sufijo(); const correo = `recurrente.${s}@pruebafspm.mx`; const folios: string[] = []; for (const [i, ruta] of ["/", "/contacto"].entries()) { await page.goto(ruta, { waitUntil: "domcontentloaded" }); const forma = page.locator("form").filter({ has: page.locator('input[name="nombre"]') }).first(); await forma.scrollIntoViewIfNeeded(); await forma.locator('input[name="nombre"]').fill(`Contacto Recurrente ${s}`); await forma.locator('input[name="email"]').fill(correo); await forma.locator('input[name="telefono"]').fill("5581000003"); await forma.locator('input[name="privacidad"]').check(); await forma.getByRole("button", { name: /solicitar/i }).click(); const aviso = page.getByText(/OPP-\d{4}-\d{4}/); await expect(aviso).toBeVisible({ timeout: 15_000 }); const texto = (await aviso.first().textContent()) ?? ""; const folio = texto.match(/OPP-\d{4}-\d{4}/)?.[0]; expect(folio, `envío ${i + 1} debe devolver folio`).toBeTruthy(); folios.push(folio!); } expect(folios[0]).not.toBe(folios[1]); // el mismo contacto, dos oportunidades: es el requisito explícito del cliente }); test("el widget de WhatsApp registra antes de abrir la conversación", async ({ page, context }) => { const s = sufijo(); await context.grantPermissions([]); 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(); await panel.locator('input[name="nombre"]').fill(`Prueba WhatsApp ${s}`); await panel.locator('input[name="telefono"]').fill("5581000004"); await panel.locator('input[name="email"]').fill(`whats.${s}@pruebafspm.mx`); await panel.locator('select[name="motivo"]').selectOption("urgencia"); await panel.locator('input[name="privacidad"]').check(); const respuesta = page.waitForResponse((r) => r.url().includes("/api/leads") && r.request().method() === "POST"); await panel.getByRole("button", { name: /abrir whatsapp/i }).click(); const res = await respuesta; const json = (await res.json()) as { ok: boolean; folio?: string; whatsappUrl?: string }; expect(json.ok, "el widget debe registrar el contacto en el CRM").toBe(true); expect(json.folio).toMatch(/^OPP-\d{4}-\d{4}$/); expect(json.whatsappUrl, "debe devolver el enlace a la conversación").toBeTruthy(); const url = new URL(json.whatsappUrl!); expect(url.hostname).toContain("whatsapp.com"); const mensaje = url.searchParams.get("text") ?? ""; expect(mensaje, "el mensaje debe llevar el prefijo de demostración").toContain("[DEMO]"); expect(mensaje, "el mensaje debe llevar el folio para rastrear la conversación").toContain(json.folio!); await expect(panel.getByText(/contacto registrado/i)).toBeVisible(); }); test("el formulario rechaza datos incompletos sin perder lo escrito", async ({ page }) => { await page.goto("/contacto", { waitUntil: "domcontentloaded" }); const forma = page.locator("form").filter({ has: page.locator('input[name="nombre"]') }).first(); await forma.locator('input[name="nombre"]').fill("Ana"); await forma.locator('input[name="email"]').fill("esto-no-es-un-correo"); await forma.locator('input[name="telefono"]').fill("123"); await forma.locator('input[name="privacidad"]').check(); await forma.getByRole("button", { name: /solicitar/i }).click(); // no debe declarar éxito await expect(page.getByText(/solicitud registrada/i)).toHaveCount(0); // y lo que el usuario escribió sigue ahí await expect(forma.locator('input[name="nombre"]')).toHaveValue("Ana"); }); test("la trampa antispam descarta el envío sin escribir nada", async ({ request }) => { const s = sufijo(); const res = await request.post("/api/leads", { data: { nombre: "Bot Automático", email: `bot.${s}@pruebafspm.mx`, telefono: "5581000009", origen: "FORMULARIO", canal: "home", privacidad: true, _sitio: "http://sitio-de-spam.example", }, }); expect(res.status()).toBe(200); const json = (await res.json()) as { ok: boolean; folio: string }; // responde en falso positivo para no enseñarle al bot que fue detectado expect(json.ok).toBe(true); expect(json.folio).toBe("OPP-0000-0000"); });