352 lines
18 KiB
JavaScript
352 lines
18 KiB
JavaScript
// Campos del editor: contadores, barra de formato, variables, subida de archivos
|
||
// (input y arrastrar-soltar), cuenta, sub-editores y validación.
|
||
import {
|
||
test, expect, estado, vaciar, anadirTipo, campo, filaChat, tarjeta, abrirTarjeta, irA, PNG_1PX,
|
||
} from "./fixtures.mjs";
|
||
|
||
const contadorDe = (page, ruta) =>
|
||
page.locator(".ed-campo", { has: page.locator(`[data-ruta="${ruta}"]`) }).first().locator(".ed-contador");
|
||
|
||
/** Selecciona [ini, fin) en un input/textarea. */
|
||
async function seleccionar(loc, ini, fin) {
|
||
await loc.evaluate((el, [a, b]) => { el.focus(); el.setSelectionRange(a, b); }, [ini, fin]);
|
||
}
|
||
|
||
/** Suelta un archivo sobre un elemento (drag & drop del sistema). */
|
||
async function soltarArchivo(page, destino, { nombre, tipo, base64 }) {
|
||
const dt = await page.evaluateHandle(({ nombre, tipo, base64 }) => {
|
||
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
||
const d = new DataTransfer();
|
||
d.items.add(new File([bytes], nombre, { type: tipo }));
|
||
return d;
|
||
}, { nombre, tipo, base64 });
|
||
await destino.dispatchEvent("dragenter", { dataTransfer: dt });
|
||
await destino.dispatchEvent("dragover", { dataTransfer: dt });
|
||
await destino.dispatchEvent("drop", { dataTransfer: dt });
|
||
}
|
||
|
||
test.describe("Contadores de límites", () => {
|
||
test("n / límite, ámbar al 90 % y rojo al pasarse sin cortar la escritura", async ({ app }) => {
|
||
await vaciar(app);
|
||
await anadirTipo(app, "buttons");
|
||
const ruta = "mensajes.0.botones.0.texto";
|
||
const cont = contadorDe(app, ruta);
|
||
await campo(app, ruta).fill("Hola");
|
||
await expect(cont).toHaveText("4 / 20");
|
||
await expect(cont).not.toHaveClass(/ed-contador-(alerta|over)/);
|
||
await campo(app, ruta).fill("x".repeat(18));
|
||
await expect(cont).toHaveText("18 / 20");
|
||
await expect(cont).toHaveClass(/ed-contador-alerta/);
|
||
await campo(app, ruta).fill("x".repeat(23));
|
||
await expect(cont).toHaveText("23 / 20");
|
||
await expect(cont).toHaveClass(/ed-contador-over/);
|
||
await expect(campo(app, ruta)).toHaveValue("x".repeat(23));
|
||
// la tarjeta avisa de que la Cloud API lo rechazaría
|
||
await expect(tarjeta(app, 0).locator(".ed-chip-aviso")).toBeVisible();
|
||
await expect(tarjeta(app, 0).locator(".ed-validacion")).toContainText("rechazaría");
|
||
await expect(tarjeta(app, 0).locator(".ed-validacion")).toContainText("supera 20");
|
||
await expect(app.locator("#ed-avisos-global")).toBeVisible();
|
||
});
|
||
|
||
test("el cuerpo de texto cuenta hasta 4096", async ({ app }) => {
|
||
await vaciar(app);
|
||
await anadirTipo(app, "text");
|
||
await campo(app, "mensajes.0.texto").fill("abc");
|
||
await expect(contadorDe(app, "mensajes.0.texto")).toHaveText("3 / 4096");
|
||
});
|
||
});
|
||
|
||
test.describe("Barra de formato y variables", () => {
|
||
for (const [boton, marca, etiqueta] of [
|
||
["ed-fmt-negrita", "*", "strong"], ["ed-fmt-cursiva", "_", "em"],
|
||
["ed-fmt-tachado", "~", "del"], ["ed-fmt-mono", "`", "code"],
|
||
]) {
|
||
test(`${boton} envuelve la selección con ${marca}`, async ({ app }) => {
|
||
await vaciar(app);
|
||
const id = await anadirTipo(app, "text");
|
||
const ta = campo(app, "mensajes.0.texto");
|
||
await ta.fill("Hola mundo feliz");
|
||
await seleccionar(ta, 5, 10);
|
||
await tarjeta(app, 0).locator(`.ed-fmtbar .${boton}`).click();
|
||
await expect(ta).toHaveValue(`Hola ${marca}mundo${marca} feliz`);
|
||
// el cursor mantiene la palabra seleccionada
|
||
const sel = await ta.evaluate((el) => [el.selectionStart, el.selectionEnd]);
|
||
expect(sel).toEqual([6, 11]);
|
||
await expect(filaChat(app, id).locator(`.wa-texto ${etiqueta}`)).toHaveText("mundo");
|
||
});
|
||
}
|
||
|
||
test("sin selección inserta el par de marcas con el cursor en medio", async ({ app }) => {
|
||
await vaciar(app);
|
||
await anadirTipo(app, "text");
|
||
const ta = campo(app, "mensajes.0.texto");
|
||
await ta.fill("ab");
|
||
await seleccionar(ta, 1, 1);
|
||
await tarjeta(app, 0).locator(".ed-fmtbar .ed-fmt-negrita").click();
|
||
await expect(ta).toHaveValue("a**b");
|
||
expect(await ta.evaluate((el) => [el.selectionStart, el.selectionEnd])).toEqual([2, 2]);
|
||
});
|
||
|
||
test("la barra de formato existe en plantilla y lista", async ({ app }) => {
|
||
await vaciar(app);
|
||
await anadirTipo(app, "template");
|
||
await anadirTipo(app, "list");
|
||
await expect(tarjeta(app, 0).locator(".ed-fmtbar").first()).toBeVisible();
|
||
await expect(tarjeta(app, 1).locator(".ed-fmtbar").first()).toBeVisible();
|
||
});
|
||
|
||
test("«+ Añadir variable» inserta {{n}} siguiente en el cursor", async ({ app }) => {
|
||
await vaciar(app);
|
||
const id = await anadirTipo(app, "template");
|
||
const body = campo(app, "mensajes.0.body");
|
||
await body.fill("Hola {{1}}, tu pedido");
|
||
await seleccionar(body, 21, 21);
|
||
const btnVar = app.locator(".ed-campo", { has: app.locator('[data-ruta="mensajes.0.body"]') }).locator('[data-acc="var"]');
|
||
await btnVar.click();
|
||
await expect(body).toHaveValue("Hola {{1}}, tu pedido{{2}}");
|
||
await btnVar.click();
|
||
await expect(body).toHaveValue("Hola {{1}}, tu pedido{{2}}{{3}}");
|
||
await expect(filaChat(app, id).locator(".tpl-body")).toHaveText("Hola {{1}}, tu pedido{{2}}{{3}}");
|
||
});
|
||
});
|
||
|
||
test.describe("Subida de archivos", () => {
|
||
test("imagen por archivo: dataURL en el chat y persiste tras recargar", async ({ app }) => {
|
||
await vaciar(app);
|
||
const id = await anadirTipo(app, "image");
|
||
const t = tarjeta(app, 0);
|
||
await t.locator('input[type="file"]').first().setInputFiles({ name: "foto.png", mimeType: "image/png", buffer: PNG_1PX });
|
||
await expect.poll(async () => (await estado(app)).mensajes[0].src).toMatch(/^data:image\/png;base64,/);
|
||
await expect(filaChat(app, id).locator("img.wa-media")).toHaveAttribute("src", /^data:image\/png;base64,/);
|
||
await expect(t.locator(".ed-media-vista").first()).toBeVisible(); // miniatura en el editor
|
||
await app.reload();
|
||
await expect(filaChat(app, id).locator("img.wa-media")).toHaveAttribute("src", /^data:image\/png;base64,/);
|
||
});
|
||
|
||
test("clic en la zona de soltado abre el selector de archivos", async ({ app }) => {
|
||
await vaciar(app);
|
||
await anadirTipo(app, "sticker");
|
||
const [chooser] = await Promise.all([
|
||
app.waitForEvent("filechooser"),
|
||
tarjeta(app, 0).locator('[data-acc="drop-clic"]').first().click(),
|
||
]);
|
||
await chooser.setFiles({ name: "sticker.png", mimeType: "image/png", buffer: PNG_1PX });
|
||
await expect.poll(async () => (await estado(app)).mensajes[0].src).toMatch(/^data:image\/png/);
|
||
});
|
||
|
||
test("arrastrar y soltar un archivo en la zona lo carga", async ({ app }) => {
|
||
await vaciar(app);
|
||
const id = await anadirTipo(app, "carousel");
|
||
const zona = tarjeta(app, 0).locator('.ed-dropzona[data-media-ruta="mensajes.0.cards.1.media"]');
|
||
await zona.scrollIntoViewIfNeeded();
|
||
await soltarArchivo(app, zona, { nombre: "card.png", tipo: "image/png", base64: PNG_1PX.toString("base64") });
|
||
await expect.poll(async () => (await estado(app)).mensajes[0].cards[1].media).toMatch(/^data:image\/png/);
|
||
await expect(filaChat(app, id).locator(".card").nth(1).locator("img.card-media")).toHaveAttribute("src", /^data:image\/png/);
|
||
});
|
||
|
||
test("soltar un archivo fuera de una zona no navega ni cambia nada", async ({ app }) => {
|
||
const antes = await estado(app);
|
||
const url = app.url();
|
||
await soltarArchivo(app, app.locator("#lista-mensajes"), { nombre: "x.png", tipo: "image/png", base64: PNG_1PX.toString("base64") });
|
||
expect(app.url()).toBe(url);
|
||
expect((await estado(app)).mensajes.length).toBe(antes.mensajes.length);
|
||
});
|
||
|
||
test("documento: subir archivo mantiene el nombre con extensión", async ({ app }) => {
|
||
await vaciar(app);
|
||
const id = await anadirTipo(app, "document");
|
||
await campo(app, "mensajes.0.filename").fill("contrato.docx");
|
||
await expect(filaChat(app, id).locator(".doc-nombre")).toHaveText("contrato.docx");
|
||
await expect(filaChat(app, id).locator(".doc-sub")).toContainText(/docx/i);
|
||
});
|
||
});
|
||
|
||
test.describe("Cuenta", () => {
|
||
test("nombre, subtítulo y verificado se reflejan en la cabecera del chat", async ({ app }) => {
|
||
await app.locator("#bloque-cuenta > summary").click();
|
||
await app.locator("#cuenta-nombre").fill("Óptica Luz");
|
||
await expect(app.locator("#chat-nombre")).toHaveText("Óptica Luz");
|
||
await expect(app.locator("#cuenta-resumen")).toContainText("Óptica Luz");
|
||
await app.locator("#cuenta-subtitulo").fill("responde en minutos");
|
||
await expect(app.locator("#chat-subtitulo")).toHaveText("responde en minutos");
|
||
await expect(app.locator("#chat-verificado")).toBeVisible();
|
||
await app.locator("#cuenta-verificado").uncheck({ force: true });
|
||
await expect(app.locator("#chat-verificado")).toBeHidden();
|
||
expect((await estado(app)).cuenta).toMatchObject({ nombre: "Óptica Luz", subtitulo: "responde en minutos", verificado: false });
|
||
});
|
||
|
||
test("avatar por archivo, por URL y quitar", async ({ app }) => {
|
||
await app.locator("#bloque-cuenta > summary").click();
|
||
await app.locator("#cuenta-avatar-archivo").setInputFiles({ name: "logo.png", mimeType: "image/png", buffer: PNG_1PX });
|
||
await expect(app.locator("#chat-avatar")).toHaveAttribute("src", /^data:image\/png/);
|
||
await expect(app.locator("#cuenta-avatar-vista")).toHaveAttribute("src", /^data:image\/png/);
|
||
await app.locator('#editor-cuenta [data-acc="avatar-quitar"]').click();
|
||
await expect(app.locator("#chat-avatar")).toHaveAttribute("src", /^data:image\/svg\+xml/);
|
||
expect((await estado(app)).cuenta.avatar).toBe("");
|
||
const dataUrl = "data:image/png;base64," + PNG_1PX.toString("base64");
|
||
const url = app.locator("#cuenta-avatar-url");
|
||
if (!(await url.isVisible())) await app.locator('#editor-cuenta label.ed-radio:has(input[value="url"])').click();
|
||
await url.fill(dataUrl);
|
||
await expect(app.locator("#chat-avatar")).toHaveAttribute("src", dataUrl);
|
||
});
|
||
});
|
||
|
||
test.describe("Sub-editores de botones, secciones, filas, tarjetas y respuestas", () => {
|
||
test("botones rápidos: añadir hasta 3 (máximo de la API) y borrar", async ({ app }) => {
|
||
await vaciar(app);
|
||
const id = await anadirTipo(app, "buttons");
|
||
const add = tarjeta(app, 0).locator('[data-acc="add-boton"]');
|
||
await add.click();
|
||
await add.click();
|
||
await expect(filaChat(app, id).locator("button.qr-btn")).toHaveCount(3);
|
||
if (await add.isEnabled()) await add.click();
|
||
expect((await estado(app)).mensajes[0].botones).toHaveLength(3);
|
||
await tarjeta(app, 0).locator('[data-acc="del-boton"]').nth(1).click();
|
||
await expect(filaChat(app, id).locator("button.qr-btn")).toHaveCount(2);
|
||
});
|
||
|
||
test("plantilla con >3 botones: 2 visibles + «Ver todas las opciones»", async ({ 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();
|
||
expect((await estado(app)).mensajes[0].botones).toHaveLength(4);
|
||
const fila = filaChat(app, id);
|
||
await expect(fila.locator(".qr-btn:visible")).toHaveCount(3);
|
||
await expect(fila.locator("[data-ver-todas]")).toHaveText(/Ver todas las opciones/);
|
||
await fila.locator("[data-ver-todas]").click();
|
||
await expect(app.locator("#sheet .sheet-titulo")).toHaveText("Todas las opciones");
|
||
await expect(app.locator("#sheet [data-opcion]")).toHaveCount(4);
|
||
});
|
||
|
||
test("plantilla: botón con URL se pinta como enlace", async ({ app }) => {
|
||
await vaciar(app);
|
||
const id = await anadirTipo(app, "template");
|
||
await campo(app, "mensajes.0.botones.0.url").fill("https://example.org/pago");
|
||
const a = filaChat(app, id).locator("a.qr-btn");
|
||
await expect(a).toHaveAttribute("href", "https://example.org/pago");
|
||
await expect(filaChat(app, id).locator("button.qr-btn[data-btn]")).toHaveCount(0);
|
||
});
|
||
|
||
test("lista: añadir sección y fila", async ({ app }) => {
|
||
await vaciar(app);
|
||
await anadirTipo(app, "list");
|
||
await tarjeta(app, 0).locator('[data-acc="add-fila"]').first().click();
|
||
await tarjeta(app, 0).locator('[data-acc="add-sec"]').click();
|
||
const e = await estado(app);
|
||
expect(e.mensajes[0].secciones).toHaveLength(2);
|
||
expect(e.mensajes[0].secciones[0].filas).toHaveLength(2);
|
||
await campo(app, "mensajes.0.secciones.1.titulo").fill("Extras");
|
||
await campo(app, "mensajes.0.secciones.1.filas.0.titulo").fill("Envío exprés");
|
||
await filaChat(app, e.mensajes[0].id).locator("button.qr-btn").click();
|
||
await expect(app.locator("#sheet .sheet-seccion").nth(1)).toHaveText("Extras");
|
||
await expect(app.locator("#sheet .sheet-fila-titulo").last()).toHaveText("Envío exprés");
|
||
});
|
||
|
||
test("carrusel: añadir y borrar tarjetas (2–5)", async ({ app }) => {
|
||
await vaciar(app);
|
||
const id = await anadirTipo(app, "carousel");
|
||
const add = tarjeta(app, 0).locator('[data-acc="add-card"]');
|
||
await add.click();
|
||
await expect(filaChat(app, id).locator(".card")).toHaveCount(3);
|
||
await tarjeta(app, 0).locator('[data-acc="del-card"]').first().click();
|
||
await expect(filaChat(app, id).locator(".card")).toHaveCount(2);
|
||
// no baja de 2
|
||
await tarjeta(app, 0).locator('[data-acc="del-card"]').first().click();
|
||
expect((await estado(app)).mensajes[0].cards.length).toBeGreaterThanOrEqual(2);
|
||
});
|
||
|
||
test("respuestas encadenadas: añadir, editar, cambiar de tipo y borrar", async ({ app }) => {
|
||
await vaciar(app);
|
||
await anadirTipo(app, "buttons");
|
||
const t = tarjeta(app, 0);
|
||
await expect(t.locator(".ed-chip-flujo")).toHaveClass(/ed-chip-flujo-incompleto/);
|
||
await t.locator('[data-acc="add-resp"][data-ruta="mensajes.0.botones.0.respuestas"]').click();
|
||
const ta = campo(app, "mensajes.0.botones.0.respuestas.0.texto");
|
||
await expect(ta).toBeFocused();
|
||
await ta.fill("Respuesta encadenada");
|
||
await expect(t.locator(".ed-chip-flujo")).not.toHaveClass(/ed-chip-flujo-incompleto/);
|
||
let r = (await estado(app)).mensajes[0].botones[0].respuestas;
|
||
expect(r).toHaveLength(1);
|
||
expect(r[0]).toMatchObject({ dir: "in", tipo: "text", texto: "Respuesta encadenada" });
|
||
await t.locator('select[data-rol="tipo-resp"]').first().selectOption("image");
|
||
await expect.poll(async () => (await estado(app)).mensajes[0].botones[0].respuestas[0].tipo).toBe("image");
|
||
r = (await estado(app)).mensajes[0].botones[0].respuestas;
|
||
expect(r).toHaveLength(1);
|
||
expect(r[0]).toMatchObject({ dir: "in", tipo: "image", src: "" });
|
||
await expect(t.locator('[data-ruta="mensajes.0.botones.0.respuestas.0.caption"]')).toBeVisible();
|
||
await t.locator('[data-acc="del-resp"]').first().click();
|
||
r = (await estado(app)).mensajes[0].botones[0].respuestas;
|
||
expect(r).toHaveLength(0);
|
||
});
|
||
|
||
test("autoPulsar se elige en el editor y se guarda como índice", async ({ app }) => {
|
||
const e = await estado(app);
|
||
const i = e.mensajes.findIndex((m) => m.tipo === "buttons");
|
||
await abrirTarjeta(app, i);
|
||
const sel = campo(app, `mensajes.${i}.autoPulsar`);
|
||
const valor = await sel.locator("option").nth(2).getAttribute("value");
|
||
await sel.selectOption(valor);
|
||
await expect.poll(async () => (await estado(app)).mensajes[i].autoPulsar).toBe(Number(valor));
|
||
await app.reload();
|
||
expect((await estado(app)).mensajes[i].autoPulsar).toBe(Number(valor));
|
||
// «vacío» quita la pulsación automática
|
||
await abrirTarjeta(app, i);
|
||
await campo(app, `mensajes.${i}.autoPulsar`).selectOption("");
|
||
await expect.poll(async () => "autoPulsar" in (await estado(app)).mensajes[i]).toBe(false);
|
||
});
|
||
});
|
||
|
||
test.describe("Validación", () => {
|
||
test("ubicación con latitud fuera de rango y contacto sin nombre muestran avisos", async ({ app }) => {
|
||
await vaciar(app);
|
||
await anadirTipo(app, "location");
|
||
await campo(app, "mensajes.0.latitud").fill("123");
|
||
await expect(tarjeta(app, 0).locator(".ed-validacion")).toContainText("Latitud no válida");
|
||
await anadirTipo(app, "contact");
|
||
await campo(app, "mensajes.1.nombre").fill("");
|
||
await expect(tarjeta(app, 1).locator(".ed-validacion")).toContainText("necesita un nombre");
|
||
await expect(app.locator("#ed-avisos-global")).toBeVisible();
|
||
await app.locator("#ed-avisos-global").click();
|
||
await expect(tarjeta(app, 0)).toHaveClass(/ed-abierta/);
|
||
});
|
||
|
||
test("la demo de ejemplo no tiene avisos de validación", async ({ app }) => {
|
||
await expect(app.locator("#lista-mensajes .ed-chip-aviso")).toHaveCount(0);
|
||
await expect(app.locator("#ed-avisos-global")).toBeHidden();
|
||
});
|
||
});
|
||
|
||
test.describe("Persistencia", () => {
|
||
test("F5 conserva todo el estado editado", async ({ app }) => {
|
||
await vaciar(app);
|
||
await anadirTipo(app, "text");
|
||
await campo(app, "mensajes.0.texto").fill("Persistente");
|
||
await anadirTipo(app, "cta_url");
|
||
const antes = await estado(app);
|
||
await app.reload();
|
||
await expect(app.locator("#lista-mensajes > .ed-tarjeta")).toHaveCount(2);
|
||
expect(await estado(app)).toEqual(antes);
|
||
await expect(filaChat(app, antes.mensajes[0].id)).toContainText("Persistente");
|
||
});
|
||
|
||
test("el indicador de guardado pasa por «Guardando…» y vuelve a «Guardado»", async ({ app }) => {
|
||
await vaciar(app);
|
||
await anadirTipo(app, "text");
|
||
await campo(app, "mensajes.0.texto").fill("x");
|
||
await expect(app.locator("#estado-guardado")).toHaveAttribute("data-estado", "ok", { timeout: 4000 });
|
||
await expect(app.locator("#estado-guardado .tb-guardado-txt")).toContainText("Guardado");
|
||
});
|
||
|
||
test("un localStorage corrupto no rompe el arranque: carga la demo", async ({ page }) => {
|
||
await page.addInitScript(() => {
|
||
if (!sessionStorage.getItem("sembrado")) {
|
||
localStorage.setItem("wam-mockup-v1", "{esto no es json");
|
||
sessionStorage.setItem("sembrado", "1");
|
||
}
|
||
});
|
||
await irA(page, "/index.html");
|
||
await expect(page.locator("#chat-nombre")).toHaveText("Café Aurora");
|
||
});
|
||
});
|