Files
whatsapp-api-mockup/tests/e2e/editor-campos.spec.mjs
T

352 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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");
});
});