Files
whatsapp-api-mockup/tests/e2e/flujos.spec.mjs
T

223 lines
12 KiB
JavaScript

// Interactividad del teléfono: quick replies, plantilla, lista + hoja, carrusel,
// «Ver todas las opciones», respuestas encadenadas editadas, composer y ticks.
import { test, expect, estado, filaChat, filasChat, vaciar, anadirTipo, campo, tarjeta, rapido } from "./fixtures.mjs";
const salientesSesion = (page) => page.locator('#chat-cuerpo > li.wa-row.out[data-id^="s"]');
const entrantesSesion = (page) => page.locator('#chat-cuerpo > li.wa-row.in[data-id^="s"]');
async function idDe(page, tipo) {
return (await estado(page)).mensajes.find((m) => m.tipo === tipo).id;
}
test.describe("Flujos al pulsar botones", () => {
test("quick reply: burbuja OUT, «escribiendo…» y respuestas encadenadas", async ({ app }) => {
const id = await idDe(app, "buttons");
const boton = filaChat(app, id).locator("button.qr-btn", { hasText: "Promociones" });
await boton.scrollIntoViewIfNeeded();
await boton.click();
// burbuja saliente inmediata con el texto del botón y la cita del mensaje
await expect(salientesSesion(app).last()).toContainText("Promociones");
await expect(salientesSesion(app).last().locator(".wa-cita")).toBeVisible();
await expect(boton).toHaveClass(/usado/);
// ticks sent → delivered → read
await expect(salientesSesion(app).last().locator(".tick-read")).toBeVisible({ timeout: 4000 });
// escribiendo… y llegada de las 2 respuestas
await expect(app.locator("#chat-escribiendo")).toBeVisible({ timeout: 4000 });
await expect(app.locator("#chat-subtitulo")).toHaveText("escribiendo…");
await expect(entrantesSesion(app)).toHaveCount(2, { timeout: 8000 });
await expect(entrantesSesion(app).nth(0)).toContainText("cold brew");
await expect(entrantesSesion(app).nth(1)).toContainText("croissant");
await expect(app.locator("#chat-escribiendo")).toBeHidden();
await expect(app.locator("#chat-subtitulo")).toHaveText("en línea");
// scroll al fondo
const cuerpo = app.locator("#chat-cuerpo");
await expect.poll(() => cuerpo.evaluate((el) => el.scrollHeight - el.scrollTop - el.clientHeight)).toBeLessThan(8);
// un botón usado no se puede volver a pulsar
await boton.click({ force: true });
await app.waitForTimeout(300);
await expect(salientesSesion(app)).toHaveCount(1);
// la sesión no toca el guion ni localStorage
const guardado = await app.evaluate(() => JSON.parse(localStorage.getItem("wam-mockup-v1")));
expect((guardado?.mensajes || []).some((m) => String(m.id).startsWith("s"))).toBe(false);
});
test("plantilla: el botón «Confirmar» dispara su respuesta", async ({ app }) => {
await rapido(app);
const id = await idDe(app, "template");
await filaChat(app, id).locator("button.qr-btn", { hasText: "Confirmar" }).click();
await expect(salientesSesion(app).last()).toContainText("Confirmar");
await expect(entrantesSesion(app).last()).toContainText("Reserva confirmada", { timeout: 6000 });
});
test("lista: el botón abre la hoja; una fila dispara su flujo", async ({ app }) => {
await rapido(app);
const id = await idDe(app, "list");
await filaChat(app, id).locator("button.qr-btn").click();
const hoja = app.locator("#sheet");
await expect(hoja).toBeVisible();
await expect(hoja.locator(".sheet-titulo")).toHaveText("Ver opciones");
await expect(hoja.locator(".sheet-seccion")).toHaveText(["Bebidas", "Postres"]);
await expect(hoja.locator(".sheet-fila")).toHaveCount(5);
await expect(hoja.locator(".sheet-fila").nth(1).locator(".sheet-fila-desc")).toHaveText("Suave, con leche de avena");
await hoja.locator(".sheet-fila", { hasText: "Latte de avena" }).click();
await expect(hoja).toHaveCount(0);
await expect(salientesSesion(app).last()).toContainText("Latte de avena");
await expect(entrantesSesion(app).last()).toContainText("Latte de avena anotado", { timeout: 6000 });
});
test("lista: la hoja se cierra con la X, con el fondo y con Escape", async ({ app }) => {
const id = await idDe(app, "list");
const abrir = () => filaChat(app, id).locator("button.qr-btn").click();
await abrir();
await app.locator("#sheet .sheet-cerrar").click();
await expect(app.locator("#sheet")).toHaveCount(0);
await abrir();
await app.locator("#sheet .sheet-fondo").click({ position: { x: 20, y: 20 } });
await expect(app.locator("#sheet")).toHaveCount(0);
await abrir();
await app.locator("#sheet .sheet-fila").first().focus();
await app.keyboard.press("Escape");
await expect(app.locator("#sheet")).toHaveCount(0);
await expect(salientesSesion(app)).toHaveCount(0);
});
test("carrusel: el botón de una card dispara su flujo", async ({ app }) => {
await rapido(app);
const id = await idDe(app, "carousel");
const btn = filaChat(app, id).locator(".card").nth(1).locator("button.qr-btn");
await btn.scrollIntoViewIfNeeded();
await btn.click();
await expect(salientesSesion(app).last()).toContainText("Pedir");
await expect(entrantesSesion(app).last()).toContainText("Cold brew de 1 L", { timeout: 6000 });
});
test("«Ver todas las opciones» reenvía la opción al botón oculto", async ({ app }) => {
await rapido(app);
await vaciar(app);
const id = await anadirTipo(app, "template");
const add = tarjeta(app, 0).locator('[data-acc="add-boton"]').first();
for (let k = 0; k < 3; k++) await add.click();
await campo(app, "mensajes.0.botones.3.texto").fill("Cuarta opción");
await filaChat(app, id).locator("[data-ver-todas]").click();
await app.locator('#sheet [data-opcion="3"]').click();
await expect(app.locator("#sheet")).toHaveCount(0);
await expect(salientesSesion(app).last()).toContainText("Cuarta opción");
await expect(filaChat(app, id).locator('[data-btn="3"]')).toHaveClass(/usado/);
});
test("respuestas editadas en el editor llegan al pulsar (incluidas anidadas)", async ({ app }) => {
await rapido(app);
await vaciar(app);
const id = await anadirTipo(app, "buttons");
await campo(app, "mensajes.0.botones.0.texto").fill("Ver menú");
await tarjeta(app, 0).locator('[data-acc="add-resp"][data-ruta="mensajes.0.botones.0.respuestas"]').click();
await campo(app, "mensajes.0.botones.0.respuestas.0.texto").fill("Aquí tienes el menú");
// segunda respuesta: una lista (respuesta interactiva encadenada)
await tarjeta(app, 0).locator('[data-acc="add-resp"][data-ruta="mensajes.0.botones.0.respuestas"]').click();
await tarjeta(app, 0).locator('select[data-rol="tipo-resp"]').nth(1).selectOption("buttons");
await expect.poll(async () => (await estado(app)).mensajes[0].botones[0].respuestas[1].tipo).toBe("buttons");
await filaChat(app, id).locator("button.qr-btn", { hasText: "Ver menú" }).click();
await expect(entrantesSesion(app)).toHaveCount(2, { timeout: 6000 });
await expect(entrantesSesion(app).nth(0)).toContainText("Aquí tienes el menú");
// la respuesta de tipo «buttons» es pulsable a su vez
const anidado = entrantesSesion(app).nth(1).locator("button.qr-btn").first();
await expect(anidado).toBeVisible();
await anidado.click();
await expect(salientesSesion(app)).toHaveCount(2);
});
test("botón sin respuestas: solo sale la burbuja con su texto", async ({ app }) => {
await rapido(app);
await vaciar(app);
const id = await anadirTipo(app, "buttons");
await filaChat(app, id).locator("button.qr-btn").click();
await expect(salientesSesion(app)).toHaveCount(1);
await app.waitForTimeout(1500);
await expect(entrantesSesion(app)).toHaveCount(0);
});
test("enlaces cta_url abren en pestaña nueva y no disparan flujo", async ({ app, context }) => {
const id = await idDe(app, "cta_url");
const a = filaChat(app, id).locator("a.qr-btn");
await expect(a).toHaveAttribute("rel", /noopener/);
// la navegación externa está bloqueada por la suite; basta con que no haya flujo
const [popup] = await Promise.all([context.waitForEvent("page").catch(() => null), a.click()]);
if (popup) await popup.close();
await expect(salientesSesion(app)).toHaveCount(0);
});
test("Enter en un botón del chat lo activa desde el teclado", async ({ app }) => {
await rapido(app);
const id = await idDe(app, "buttons");
const b = filaChat(app, id).locator("button.qr-btn").first();
await b.focus();
await app.keyboard.press("Enter");
await expect(salientesSesion(app).last()).toContainText("Horarios");
});
});
test.describe("Composer", () => {
test("Enter envía un mensaje libre con hora actual y ticks automáticos", async ({ app }) => {
const input = app.locator("#composer-input");
await input.fill("¿Tienen terraza?");
await input.press("Enter");
const burbuja = salientesSesion(app).last();
await expect(burbuja).toContainText("¿Tienen terraza?");
const hora = await app.evaluate(() => WAM.horaAhora());
await expect(burbuja.locator(".meta")).toContainText(hora.slice(0, 3));
await expect(burbuja.locator(".tick")).toBeVisible();
await expect(burbuja.locator(".tick-read")).toBeVisible({ timeout: 4000 });
await expect(input).toHaveValue("");
await expect(input).toBeFocused();
});
test("el botón enviar también envía y un texto vacío no envía nada", async ({ app }) => {
await app.locator("#composer-input").fill(" ");
await app.locator("#composer-enviar").click();
await expect(salientesSesion(app)).toHaveCount(0);
await app.locator("#composer-input").fill("Hola");
await app.locator("#composer-enviar").click();
await expect(salientesSesion(app)).toHaveCount(1);
});
test("el texto del composer se pinta como texto, sin inyectar HTML", async ({ app }) => {
await app.locator("#composer-input").fill('<img src=x onerror="window.__xss=1">');
await app.locator("#composer-input").press("Enter");
await expect(salientesSesion(app).last()).toContainText("<img src=x");
expect(await app.evaluate(() => window.__xss)).toBeUndefined();
});
test("«Reiniciar chat» descarta la sesión y vuelve al guion", async ({ app }) => {
const n = (await estado(app)).mensajes.length;
await app.locator("#composer-input").fill("temporal");
await app.locator("#composer-input").press("Enter");
await expect(filasChat(app)).toHaveCount(n + 1);
await app.locator("#btn-reiniciar").click();
await expect(filasChat(app)).toHaveCount(n);
await expect(salientesSesion(app)).toHaveCount(0);
});
test("editar el guion descarta la sesión del teléfono", async ({ app }) => {
const n = (await estado(app)).mensajes.length;
await app.locator("#composer-input").fill("temporal");
await app.locator("#composer-input").press("Enter");
await expect(filasChat(app)).toHaveCount(n + 1);
await app.locator('#nuevo-tipo [data-tipo="text"]').click();
await expect(filasChat(app)).toHaveCount(n + 1); // +1 del nuevo mensaje, sin la sesión
await expect(salientesSesion(app)).toHaveCount(0);
});
test("WAM.sesionAGuion() convierte la sesión en mensajes del guion", async ({ app }) => {
const n = (await estado(app)).mensajes.length;
await app.locator("#composer-input").fill("para el guion");
await app.locator("#composer-input").press("Enter");
const añadidos = await app.evaluate(() => WAM.sesionAGuion());
expect(añadidos).toBe(1);
const e = await estado(app);
expect(e.mensajes).toHaveLength(n + 1);
expect(e.mensajes[n]).toMatchObject({ dir: "out", tipo: "text", texto: "para el guion" });
expect(e.mensajes[n].id).toMatch(/^m/);
await expect(app.locator("#lista-mensajes > .ed-tarjeta")).toHaveCount(n + 1);
});
});