Files
whatsapp-api-mockup/tests/e2e/fixtures.mjs
T

179 lines
6.9 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.
// Fixtures y utilidades compartidas por la suite e2e.
//
// - `page` se extiende para registrar errores/warnings de consola, errores de
// página y peticiones a red externa; al terminar cada test se exige que no
// haya ninguno (criterio §5.1 y §5.10 de la SPEC). Un test puede tolerar
// mensajes concretos con `consola.permitir(/regex/)`.
// - `app` navega a index.html y espera a que el chat y el editor estén listos.
import { test as base, expect } from "@playwright/test";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
export const RAIZ = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
export const URL_FILE = pathToFileURL(path.join(RAIZ, "index.html")).href;
export const FIXTURES = path.join(path.dirname(fileURLToPath(import.meta.url)), "archivos");
/** PNG 1×1 válido (para subidas y drops). */
export const PNG_1PX = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"base64"
);
export const TIPOS = [
"text", "image", "video", "audio", "document", "sticker", "location",
"contact", "template", "buttons", "list", "cta_url", "carousel",
];
export const test = base.extend({
consola: async ({}, use) => {
const registro = {
mensajes: [],
externas: [],
permitidos: [],
permitir(re) { this.permitidos.push(re); },
};
await use(registro);
},
page: async ({ page, consola }, use, testInfo) => {
page.on("console", (m) => {
if (m.type() === "error" || m.type() === "warning") consola.mensajes.push(`[${m.type()}] ${m.text()}`);
});
page.on("pageerror", (e) => consola.mensajes.push(`[pageerror] ${e.message}`));
// Ninguna dependencia de red obligatoria: toda petición externa se registra y se aborta.
await page.route(/^(https?|wss?):\/\/(?!127\.0\.0\.1|localhost)/, (ruta) => {
consola.externas.push(ruta.request().url());
return ruta.abort();
});
await use(page);
if (testInfo.status === testInfo.expectedStatus) {
const reales = consola.mensajes.filter((t) => !consola.permitidos.some((re) => re.test(t)));
expect(reales, "errores/warnings de consola").toEqual([]);
expect(consola.externas, "peticiones a red externa").toEqual([]);
}
},
app: async ({ page }, use) => {
await irA(page, "/index.html");
await use(page);
},
});
export { expect };
/** Navega y espera al arranque completo (chat + editor pintados). */
export async function irA(page, url) {
await page.goto(url);
await esperarListo(page);
}
export async function esperarListo(page) {
await page.waitForFunction(() =>
window.WAM && WAM.estado && document.querySelector("#chat-header #chat-nombre") &&
document.querySelector("#nuevo-tipo [data-tipo]")
);
}
/** Copia profunda del estado vivo. */
export function estado(page) {
return page.evaluate(() => JSON.parse(JSON.stringify(WAM.estado)));
}
/** Estado persistido en localStorage (wam-mockup-v1). */
export function estadoGuardado(page) {
return page.evaluate(() => {
if (typeof WAM.guardarYa === "function") WAM.guardarYa();
return JSON.parse(localStorage.getItem("wam-mockup-v1") || "null");
});
}
/** Deja la demo sin mensajes (como el escenario «en blanco») y repinta todo. */
export async function vaciar(page) {
await page.evaluate(() => {
WAM.estado.mensajes = [];
WAM.guardarYa();
WAM.render();
WAM.renderEditor();
});
await expect(page.locator("#lista-mensajes > .ed-tarjeta")).toHaveCount(0);
}
/** Sustituye el estado entero (normalizado) y repinta todo. */
export async function cargarEstado(page, datos) {
await page.evaluate((d) => {
WAM.estado = WAM.normalizar(d);
WAM.guardarYa();
WAM.setTema(WAM.estado.tema);
WAM.render();
WAM.renderEditor();
}, datos);
}
export const tarjetas = (page) => page.locator("#lista-mensajes > .ed-tarjeta");
export const tarjeta = (page, i) => page.locator(`#lista-mensajes > .ed-tarjeta[data-idx="${i}"]`);
export const filaChat = (page, id) => page.locator(`#chat-cuerpo > li.wa-row[data-id="${id}"]`);
export const filasChat = (page) => page.locator("#chat-cuerpo > li.wa-row");
export const campo = (page, ruta) => page.locator(`#lista-mensajes [data-ruta="${ruta}"]`).first();
/** Despliega la tarjeta i (si está plegada) y la devuelve. */
export async function abrirTarjeta(page, i) {
const t = tarjeta(page, i);
const cab = t.locator(":scope > .ed-tarjeta-cab .ed-cab-principal");
if ((await cab.getAttribute("aria-expanded")) !== "true") await cab.click();
await expect(cab).toHaveAttribute("aria-expanded", "true");
return t;
}
/** Añade un mensaje desde la paleta y devuelve su id. */
export async function anadirTipo(page, tipo) {
const antes = (await estado(page)).mensajes.map((m) => m.id);
await page.locator(`#nuevo-tipo [data-acc="anadir-tipo"][data-tipo="${tipo}"]`).click();
const handle = await page.waitForFunction((prev) => {
const nuevo = WAM.estado.mensajes.find((m) => !prev.includes(m.id));
return nuevo ? nuevo.id : null;
}, antes);
return handle.jsonValue();
}
/** Pone un campo de media en modo URL (los vacíos arrancan en «Subir archivo»). */
export async function modoUrl(page, rutaMedia) {
const inp = campo(page, rutaMedia);
if (await inp.isVisible()) return inp;
const media = page.locator(".ed-media", { has: page.locator(`[data-ruta="${rutaMedia}"]`) }).first();
await media.locator('label.ed-radio:has(input[value="url"])').click();
await expect(inp).toBeVisible();
return inp;
}
/** Acelera los flujos del teléfono (velocidad 2x). */
export async function rapido(page) {
await page.evaluate(() => WAM.setVelocidad(2));
}
/**
* Arrastra con el ratón (HTML5 drag & drop nativo) el asa `asa` hasta `destino`.
* `abajo` = soltar en la mitad inferior del destino (queda detrás de él).
* Coloca antes el destino arriba del todo para que ambos queden a la vista.
*/
export async function arrastrar(page, asa, destino, { abajo = false } = {}) {
await destino.evaluate((el) => el.scrollIntoView({ block: "start" }));
const a = await asa.boundingBox();
const d = await destino.boundingBox();
const vh = page.viewportSize().height;
if (!a || !d || a.y < 0 || a.y + a.height > vh) throw new Error("el asa y el destino no caben a la vez en pantalla");
await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2);
await page.mouse.down();
await page.mouse.move(a.x + a.width / 2 + 4, a.y + a.height / 2 + 4, { steps: 3 });
const y = abajo ? d.y + d.height - 4 : d.y + 4;
await page.mouse.move(d.x + Math.min(40, d.width / 2), y, { steps: 8 });
await page.mouse.up();
}
/** Pulsa una acción de la cabecera de la tarjeta i (las flechas se revelan al pasar el ratón). */
export async function accion(page, i, acc, delta) {
const cab = tarjeta(page, i).locator(":scope > .ed-tarjeta-cab");
await cab.hover();
const sel = `[data-acc="${acc}"]` + (delta === undefined ? "" : `[data-delta="${delta}"]`);
await cab.locator(sel).click();
}