223 lines
12 KiB
JavaScript
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);
|
|
});
|
|
});
|