// Barra superior: tema, presentación, exportar/importar, restablecer, deshacer/rehacer, // atajos de teclado, demos guardadas y escenarios por industria. import fs from "node:fs"; import { test, expect, estado, vaciar, anadirTipo, campo, filasChat, tarjetas, cargarEstado, } from "./fixtures.mjs"; async function exportar(page, disparar) { const [descarga] = await Promise.all([page.waitForEvent("download"), disparar()]); const ruta = await descarga.path(); return { nombre: descarga.suggestedFilename(), json: JSON.parse(fs.readFileSync(ruta, "utf8")) }; } async function importar(page, datos, nombre = "demo.json") { const buffer = Buffer.from(typeof datos === "string" ? datos : JSON.stringify(datos)); await page.locator("#btn-importar").click(); const confirmar = page.locator("#dlg-confirmar"); const [chooser] = await Promise.all([ page.waitForEvent("filechooser"), (async () => { if (await confirmar.isVisible().catch(() => false)) { await confirmar.getByRole("button", { name: "Elegir archivo…" }).click(); } })(), ]); await chooser.setFiles({ name: nombre, mimeType: "application/json", buffer }); } test.describe("Tema", () => { test("alterna claro/oscuro en editor y chat y persiste", async ({ app }) => { const b = app.locator("#btn-tema"); await b.click(); await expect(app.locator("body")).toHaveAttribute("data-tema", "oscuro"); await expect(app.locator("#pantalla")).toHaveClass(/oscuro/); await expect(b).toHaveAttribute("aria-pressed", "true"); const fondo = (sel) => app.locator(sel).evaluate((el) => getComputedStyle(el).backgroundColor); const oscuroEditor = await fondo("body"); await app.reload(); await expect(app.locator("body")).toHaveAttribute("data-tema", "oscuro"); expect((await estado(app)).tema).toBe("oscuro"); await app.locator("#btn-tema").click(); await expect(app.locator("body")).toHaveAttribute("data-tema", "claro"); await expect(app.locator("#pantalla")).not.toHaveClass(/oscuro/); expect(await fondo("body")).not.toBe(oscuroEditor); }); test("la tecla T alterna el tema (y no lo hace escribiendo en un campo)", async ({ app }) => { await app.locator("#chat-header").click(); await app.keyboard.press("t"); await expect(app.locator("body")).toHaveAttribute("data-tema", "oscuro"); await app.locator("#composer-input").fill(""); await app.locator("#composer-input").press("t"); await expect(app.locator("body")).toHaveAttribute("data-tema", "oscuro"); await expect(app.locator("#composer-input")).toHaveValue("t"); }); }); test.describe("Modo presentación", () => { test("oculta el editor y centra el iPhone; Esc sale", async ({ app }) => { await app.locator("#btn-presentacion").click(); await expect(app.locator("body")).toHaveClass(/presentacion/); await expect(app.locator("#btn-presentacion")).toHaveAttribute("aria-pressed", "true"); await expect(app.locator("#panel-editor")).toBeHidden(); const vw = app.viewportSize().width; const caja = await app.locator("#iphone").boundingBox(); expect(Math.abs(caja.x + caja.width / 2 - vw / 2)).toBeLessThan(40); // el iPhone cabe entero en la ventana expect(caja.y).toBeGreaterThanOrEqual(0); expect(caja.y + caja.height).toBeLessThanOrEqual(app.viewportSize().height + 1); // el chat sigue funcionando en presentación await app.locator("#composer-input").fill("en vivo"); await app.locator("#composer-input").press("Enter"); await expect(app.locator('#chat-cuerpo li.out[data-id^="s"]')).toContainText("en vivo"); await app.locator("#chat-header").click(); await app.keyboard.press("Escape"); await expect(app.locator("body")).not.toHaveClass(/presentacion/); await expect(app.locator("#panel-editor")).toBeVisible(); }); test("la tecla P entra y sale", async ({ app }) => { await app.locator("#chat-header").click(); await app.keyboard.press("p"); await expect(app.locator("body")).toHaveClass(/presentacion/); await app.keyboard.press("p"); await expect(app.locator("body")).not.toHaveClass(/presentacion/); }); }); test.describe("Exportar e importar", () => { test("exporta .json con el estado completo", async ({ app }) => { const { nombre, json } = await exportar(app, () => app.locator("#btn-exportar").click()); expect(nombre).toBe("cafe-aurora.json"); expect(json).toEqual(await estado(app)); expect(json.mensajes.find((m) => m.tipo === "list").secciones[0].filas[0].respuestas).toHaveLength(1); }); test("sin nombre de cuenta exporta wam-demo.json; Ctrl+S también exporta", async ({ app }) => { await app.evaluate(() => { WAM.estado.cuenta.nombre = ""; WAM.guardarYa(); }); await app.locator("#chat-header").click(); const { nombre } = await exportar(app, () => app.keyboard.press("Control+s")); expect(nombre).toBe("wam-demo.json"); }); test("importar el mismo archivo restaura un estado idéntico", async ({ app }) => { const { json } = await exportar(app, () => app.locator("#btn-exportar").click()); await vaciar(app); await anadirTipo(app, "text"); await importar(app, json); await expect.poll(async () => (await estado(app)).mensajes.length).toBe(json.mensajes.length); expect(await estado(app)).toEqual(json); await expect(filasChat(app)).toHaveCount(json.mensajes.length); await expect(tarjetas(app)).toHaveCount(json.mensajes.length); await app.reload(); expect(await estado(app)).toEqual(json); }); test("importar normaliza: array de mensajes, IDs duplicados, URLs peligrosas", async ({ app }) => { await importar(app, [ { id: "x", dir: "in", tipo: "text", texto: "uno" }, { id: "x", dir: "out", tipo: "cta_url", body: "b", textoBoton: "Ir", url: "javascript:alert(1)" }, { dir: "in", tipo: "desconocido", texto: "tres" }, ]); await expect.poll(async () => (await estado(app)).mensajes.length).toBe(3); const e = await estado(app); const idsUnicos = new Set(e.mensajes.map((m) => m.id)); expect(idsUnicos.size).toBe(3); expect(e.mensajes[1].url).toBe(""); await expect(app.locator(`#chat-cuerpo li[data-id="${e.mensajes[1].id}"] a[href^="javascript"]`)).toHaveCount(0); }); test("un JSON inválido avisa y no cambia la demo", async ({ app, consola }) => { consola.permitir(/JSON|Unexpected token|SyntaxError/i); const antes = await estado(app); await importar(app, "{ no es json", "roto.json"); await expect(app.locator("#ed-toasts .ed-toast-error, #wam-aviso").first()).toBeVisible(); expect(await estado(app)).toEqual(antes); }); test("cancelar el diálogo de importar no abre el selector", async ({ app }) => { let abierto = false; app.on("filechooser", () => { abierto = true; }); await app.locator("#btn-importar").click(); await app.locator("#dlg-confirmar").getByRole("button", { name: "Cancelar" }).click(); await expect(app.locator("#dlg-confirmar")).toHaveCount(0); await app.waitForTimeout(300); expect(abierto).toBe(false); }); }); test.describe("Restablecer, deshacer y rehacer", () => { test("restablecer pide confirmación, restaura la demo y se puede deshacer", async ({ app }) => { await vaciar(app); await anadirTipo(app, "text"); await campo(app, "mensajes.0.texto").fill("Mi cambio"); const mio = await estado(app); await app.locator("#btn-mas").click(); await app.locator("#btn-reset").click(); const dlg = app.locator("#dlg-confirmar"); await expect(dlg).toBeVisible(); await expect(dlg).toContainText("1 mensaje"); await dlg.getByRole("button", { name: "Restablecer" }).click(); await expect(app.locator("#chat-nombre")).toHaveText("Café Aurora"); const e = await estado(app); expect(e.mensajes).toHaveLength(14); expect(e.mensajes[0].id).toBe("m1"); const toast = app.locator("#ed-toasts .ed-toast", { hasText: "restablecida" }); await toast.locator(".ed-toast-accion").click(); await expect.poll(async () => (await estado(app)).mensajes).toEqual(mio.mensajes); }); test("«Exportar antes» del diálogo descarga la demo actual", async ({ app }) => { await app.locator("#btn-mas").click(); await app.locator("#btn-reset").click(); const { nombre } = await exportar(app, () => app.locator("#dlg-confirmar").getByRole("button", { name: "Exportar antes" }).click()); expect(nombre).toBe("cafe-aurora.json"); await expect(app.locator("#dlg-confirmar").getByRole("button", { name: "Exportada ✓" })).toBeDisabled(); await app.locator("#dlg-confirmar").getByRole("button", { name: "Cancelar" }).click(); }); test("sin mensajes restablece sin preguntar", async ({ app }) => { await vaciar(app); await app.locator("#btn-mas").click(); await app.locator("#btn-reset").click(); await expect(app.locator("#dlg-confirmar")).toHaveCount(0); await expect(tarjetas(app)).toHaveCount(14); }); test("botones y atajos de deshacer / rehacer", async ({ app }) => { await expect(app.locator("#btn-deshacer")).toBeDisabled(); const n = (await estado(app)).mensajes.length; await anadirTipo(app, "contact"); await expect(app.locator("#btn-deshacer")).toBeEnabled(); await app.locator("#btn-deshacer").click(); await expect.poll(async () => (await estado(app)).mensajes.length).toBe(n); await expect(app.locator("#btn-rehacer")).toBeEnabled(); await app.locator("#btn-rehacer").click(); await expect.poll(async () => (await estado(app)).mensajes.length).toBe(n + 1); await app.locator("#chat-header").click(); await app.keyboard.press("Control+z"); await expect.poll(async () => (await estado(app)).mensajes.length).toBe(n); await app.keyboard.press("Control+Shift+z"); await expect.poll(async () => (await estado(app)).mensajes.length).toBe(n + 1); await expect(tarjetas(app)).toHaveCount(n + 1); await expect(filasChat(app)).toHaveCount(n + 1); }); test("deshacer una ráfaga de escritura vuelve al texto previo", async ({ app }) => { await vaciar(app); await anadirTipo(app, "text"); const ta = campo(app, "mensajes.0.texto"); const original = await ta.inputValue(); await ta.fill("uno"); await ta.pressSequentially(" dos"); await app.locator("#btn-deshacer").click(); await expect.poll(async () => (await estado(app)).mensajes[0]?.texto).toBe(original); }); }); test.describe("Atajos y demos", () => { test("«?» y el botón de atajos abren la ayuda", async ({ app }) => { await app.locator("#btn-atajos").click(); const dlg = app.locator("#dlg-atajos"); await expect(dlg).toBeVisible(); await expect(dlg).toContainText("Modo presentación"); await dlg.locator(".wam-dlg-cerrar").click(); await expect(dlg).toHaveCount(0); await app.locator("#chat-header").click(); await app.keyboard.press("?"); await expect(app.locator("#dlg-atajos")).toBeVisible(); await app.keyboard.press("Escape"); await expect(app.locator("#dlg-atajos")).toHaveCount(0); }); test("escenarios por industria: cargar uno y deshacer", async ({ app }) => { const escenarios = await app.evaluate(() => WAM.ESCENARIOS.map((e) => ({ id: e.id, nombre: e.nombre }))); expect(escenarios.length).toBeGreaterThanOrEqual(7); await app.locator("#btn-demos").click(); const dlg = app.locator("#dlg-demos"); await expect(dlg).toBeVisible(); await expect(dlg.locator(".wam-esc")).toHaveCount(escenarios.length + 1); const elegido = escenarios.find((e) => e.id !== "blanco" && !/blanco/i.test(e.nombre)) || escenarios[1]; const datos = await app.evaluate((id) => WAM.normalizar(WAM.datosEscenario(id)), elegido.id); await dlg.locator(`.wam-esc[data-esc="${elegido.id}"]`).click(); await expect(dlg).toHaveCount(0); await expect(app.locator("#chat-nombre")).toHaveText(datos.cuenta.nombre || "Chat"); await expect(filasChat(app)).toHaveCount(datos.mensajes.length); await app.locator("#ed-toasts .ed-toast", { hasText: elegido.nombre }).locator(".ed-toast-accion").click(); await expect(app.locator("#chat-nombre")).toHaveText("Café Aurora"); }); test("todos los escenarios cargan sin avisos de validación ni errores", async ({ app }) => { const ids = await app.evaluate(() => WAM.ESCENARIOS.map((e) => e.id)); for (const id of ids) { await app.locator("#btn-demos").click(); await app.locator(`#dlg-demos .wam-esc[data-esc="${id}"]`).click(); const e = await estado(app); await expect(filasChat(app)).toHaveCount(e.mensajes.length); await expect(tarjetas(app)).toHaveCount(e.mensajes.length); await expect(app.locator("#lista-mensajes .ed-chip-aviso"), `escenario ${id}`).toHaveCount(0); } }); test("demos guardadas: guardar, abrir, renombrar, duplicar y borrar", async ({ app }) => { // 1) guardar la actual con nombre await app.locator("#btn-demos").click(); let dlg = app.locator("#dlg-demos"); await dlg.locator("#dlg-demo-nombre").fill("Cliente Aurora"); await dlg.getByRole("button", { name: /Guardar/ }).click(); await expect(dlg.locator(".wam-demo")).toHaveCount(1); await expect(dlg.locator(".wam-demo-nombre")).toHaveText("Cliente Aurora"); await expect(dlg.locator(".wam-demo")).toHaveClass(/wam-demo-activa/); // el export usa el nombre de la demo activa await dlg.locator(".wam-dlg-pie").getByRole("button", { name: "Cerrar" }).click(); const { nombre } = await exportar(app, () => app.locator("#btn-exportar").click()); expect(nombre).toBe("cliente-aurora.json"); // 2) cambiar la demo y volver a abrir la guardada await vaciar(app); await app.locator("#btn-demos").click(); dlg = app.locator("#dlg-demos"); await dlg.locator(".wam-demo").getByRole("button", { name: "Abrir" }).click(); await expect(tarjetas(app)).toHaveCount(14); // 3) renombrar, duplicar y borrar await app.locator("#btn-demos").click(); dlg = app.locator("#dlg-demos"); await dlg.locator(".wam-demo").getByRole("button", { name: "Renombrar" }).click(); const inp = dlg.locator(".wam-demo input.wam-dlg-input"); await inp.fill("Aurora v2"); await inp.press("Enter"); await expect(dlg.locator(".wam-demo-nombre")).toHaveText("Aurora v2"); await dlg.locator(".wam-demo").getByRole("button", { name: "Duplicar" }).click(); await expect(dlg.locator(".wam-demo")).toHaveCount(2); await expect(dlg.locator(".wam-demo-nombre").first()).toHaveText("Aurora v2 (copia)"); const borrar = dlg.locator(".wam-demo").first().getByRole("button", { name: "Borrar" }); await borrar.click(); await expect(dlg.locator(".wam-demo").first().getByRole("button", { name: "¿Seguro?" })).toBeVisible(); await dlg.locator(".wam-demo").first().getByRole("button", { name: "¿Seguro?" }).click(); await expect(dlg.locator(".wam-demo")).toHaveCount(1); // persiste en localStorage const indice = await app.evaluate(() => JSON.parse(localStorage.getItem("wam-demos"))); expect(indice.map((d) => d.nombre)).toEqual(["Aurora v2"]); }); test("la tecla N abre las demos; F pide pantalla completa sin errores", async ({ app }) => { await app.locator("#chat-header").click(); await app.keyboard.press("n"); await expect(app.locator("#dlg-demos")).toBeVisible(); await app.keyboard.press("Escape"); await expect(app.locator("#dlg-demos")).toHaveCount(0); await app.keyboard.press("f"); await app.waitForTimeout(300); if (await app.evaluate(() => !!document.fullscreenElement)) await app.evaluate(() => document.exitFullscreen()); }); test("cargar un estado con avisoCifrado:false oculta los avisos del sistema", async ({ app }) => { await cargarEstado(app, { version: 1, tema: "claro", cuenta: { nombre: "X", avisoCifrado: false }, mensajes: [] }); await expect(app.locator("#chat-cuerpo > li.wa-sistema")).toHaveCount(0); }); });