WIP: mejoras de fidelidad, editor, lógica y suite e2e (checkpoint tras reinicio)

This commit is contained in:
urieljareth
2026-09-27 20:46:51 -06:00
parent dc6e31a9d7
commit 9a5f21badf
33 changed files with 7719 additions and 1129 deletions
+315
View File
@@ -0,0 +1,315 @@
// 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 <slug>.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-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-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-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);
});
});