// 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(''); await app.locator("#composer-input").press("Enter"); await expect(salientesSesion(app).last()).toContainText(" 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); }); });