WIP: mejoras de fidelidad, editor, lógica y suite e2e (checkpoint tras reinicio)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user