Deshacer con demo activa, avisos obsoletos, hoja de opciones y foco en hojas
- Deshacer/rehacer guardan y reponen «wam-demo-activa» con cada instantánea
({estado, activa}); «Actualizar» pide un segundo clic si el contenido cambió
de cuenta o de número de mensajes respecto a la demo guardada.
- WAM.puntoDeshacer cierra los avisos con acción: su «Deshacer» ya no
desharía el cambio que anuncian.
- «Todas las opciones» replica botonAccion: URL bloqueada o no válida se
pinta como fila inerte (aria-disabled, sin data-opcion) en el grupo de
enlaces.
- Hojas inferiores: enfocan la primera fila (o la X), retienen Tab dentro del
panel y devuelven el foco al cerrar (origen, fila repintada o
#chat-cuerpo); no roban el foco durante «Reproducir demo».
- Tests e2e de regresión en tests/e2e/regresiones.spec.mjs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1176003b42
commit
875ae9ad67
@@ -769,6 +769,7 @@
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
.wa .sheet-fila:active { background: rgba(0, 0, 0, .05); }
|
.wa .sheet-fila:active { background: rgba(0, 0, 0, .05); }
|
||||||
|
.wa .sheet-fila.qr-btn-inactiva:active { background: none; }
|
||||||
.wa .sheet-fila-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0; }
|
.wa .sheet-fila-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0; }
|
||||||
.wa .sheet-fila-titulo { font-size: 17px; line-height: 22px; }
|
.wa .sheet-fila-titulo { font-size: 17px; line-height: 22px; }
|
||||||
.wa .sheet-fila-desc { font-size: 15px; line-height: 18px; letter-spacing: 0; color: var(--wa-sheet-sub); }
|
.wa .sheet-fila-desc { font-size: 15px; line-height: 18px; letter-spacing: 0; color: var(--wa-sheet-sub); }
|
||||||
|
|||||||
+48
-2
@@ -30,6 +30,8 @@
|
|||||||
|
|
||||||
var ESCRIBIENDO = false;
|
var ESCRIBIENDO = false;
|
||||||
var HOJA_ID = ""; /* id del mensaje cuya hoja está abierta ("" = ninguna) */
|
var HOJA_ID = ""; /* id del mensaje cuya hoja está abierta ("" = ninguna) */
|
||||||
|
var ORIGEN_HOJA = null; /* elemento que tenía el foco al abrir la hoja */
|
||||||
|
var FOCO_EN_HOJA = false; /* la hoja movió el foco a su interior al abrirse */
|
||||||
|
|
||||||
/* Solo esquemas seguros en href (defensa en profundidad para JSON importados). */
|
/* Solo esquemas seguros en href (defensa en profundidad para JSON importados). */
|
||||||
function urlSegura(u) {
|
function urlSegura(u) {
|
||||||
@@ -785,7 +787,7 @@
|
|||||||
|
|
||||||
/* Hoja inferior iOS genérica (B la crea/destruye) en #pantalla.
|
/* Hoja inferior iOS genérica (B la crea/destruye) en #pantalla.
|
||||||
alClic: manejador opcional de presentación para las filas. */
|
alClic: manejador opcional de presentación para las filas. */
|
||||||
function montarHoja(titulo, contenido, pie, alClic) {
|
function montarHoja(titulo, contenido, pie, alClic, opts) {
|
||||||
var pantalla = byId("pantalla");
|
var pantalla = byId("pantalla");
|
||||||
if (!pantalla) return null;
|
if (!pantalla) return null;
|
||||||
WAM.cerrarSheet();
|
WAM.cerrarSheet();
|
||||||
@@ -799,6 +801,28 @@
|
|||||||
'<div class="sheet-opciones" id="sheet-opciones">' + contenido + "</div>" +
|
'<div class="sheet-opciones" id="sheet-opciones">' + contenido + "</div>" +
|
||||||
(pie ? '<div class="sheet-pie">' + esc(pie) + "</div>" : "") + "</div>";
|
(pie ? '<div class="sheet-pie">' + esc(pie) + "</div>" : "") + "</div>";
|
||||||
pantalla.appendChild(hoja);
|
pantalla.appendChild(hoja);
|
||||||
|
/* Foco (diálogo modal): se guarda el origen, se enfoca la primera fila
|
||||||
|
activa (o la X) y Tab / Mayús+Tab quedan dentro del panel. Durante
|
||||||
|
«Reproducir demo» (o con opts.sinFoco) no se roba el foco. */
|
||||||
|
ORIGEN_HOJA = document.activeElement && document.activeElement !== document.body ? document.activeElement : null;
|
||||||
|
var enDemo = typeof WAM.demoActiva === "function" && WAM.demoActiva();
|
||||||
|
if (!enDemo && !(opts && opts.sinFoco)) {
|
||||||
|
var primera = hoja.querySelector('.sheet-fila:not([aria-disabled="true"]):not(span)') || hoja.querySelector(".sheet-cerrar");
|
||||||
|
if (primera) { try { primera.focus({ preventScroll: true }); } catch (e) { primera.focus(); } }
|
||||||
|
FOCO_EN_HOJA = !!primera;
|
||||||
|
}
|
||||||
|
hoja.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key !== "Tab") return;
|
||||||
|
var panel = hoja.querySelector(".sheet-panel");
|
||||||
|
var focos = panel ? Array.prototype.filter.call(panel.querySelectorAll("a[href], button:not([disabled])"), function (x) {
|
||||||
|
return !x.hidden;
|
||||||
|
}) : [];
|
||||||
|
if (!focos.length) return;
|
||||||
|
var ini = focos[0], fin = focos[focos.length - 1], act = document.activeElement;
|
||||||
|
var dentro = panel.contains(act);
|
||||||
|
if (e.shiftKey && (act === ini || !dentro)) { e.preventDefault(); fin.focus(); }
|
||||||
|
else if (!e.shiftKey && (act === fin || !dentro)) { e.preventDefault(); ini.focus(); }
|
||||||
|
});
|
||||||
hoja.addEventListener("click", function (e) {
|
hoja.addEventListener("click", function (e) {
|
||||||
var t = e.target;
|
var t = e.target;
|
||||||
if (!t || !t.closest) return;
|
if (!t || !t.closest) return;
|
||||||
@@ -864,7 +888,11 @@
|
|||||||
if (url) {
|
if (url) {
|
||||||
enlaces += '<a class="sheet-fila sheet-fila-btn" href="' + esc(url) + '" target="_blank" rel="noopener noreferrer">' +
|
enlaces += '<a class="sheet-fila sheet-fila-btn" href="' + esc(url) + '" target="_blank" rel="noopener noreferrer">' +
|
||||||
icono(tipoIconoUrl(url)) + etiqueta + "</a>";
|
icono(tipoIconoUrl(url)) + etiqueta + "</a>";
|
||||||
} else if (vacio(b.url)) {
|
} else if (!vacio(b.url) || b.urlBloqueada) {
|
||||||
|
/* Igual que botonAccion: URL no permitida o bloqueada → fila inerte */
|
||||||
|
enlaces += '<span class="sheet-fila sheet-fila-btn qr-btn-inactiva" aria-disabled="true">' +
|
||||||
|
icono("url") + etiqueta + "</span>";
|
||||||
|
} else {
|
||||||
var usado = li && li.querySelector('[data-btn="' + j + '"].usado');
|
var usado = li && li.querySelector('[data-btn="' + j + '"].usado');
|
||||||
respuestas += '<button type="button" class="sheet-fila sheet-fila-btn' + (usado ? " usado" : "") + '" data-opcion="' + j + '"' +
|
respuestas += '<button type="button" class="sheet-fila sheet-fila-btn' + (usado ? " usado" : "") + '" data-opcion="' + j + '"' +
|
||||||
(usado ? ' aria-disabled="true"' : "") + ">" + icono("responder") + etiqueta + "</button>";
|
(usado ? ' aria-disabled="true"' : "") + ">" + icono("responder") + etiqueta + "</button>";
|
||||||
@@ -889,7 +917,25 @@
|
|||||||
|
|
||||||
WAM.cerrarSheet = function () {
|
WAM.cerrarSheet = function () {
|
||||||
var hoja = byId("sheet");
|
var hoja = byId("sheet");
|
||||||
|
var idHoja = HOJA_ID;
|
||||||
|
var origen = ORIGEN_HOJA;
|
||||||
|
var movido = FOCO_EN_HOJA;
|
||||||
|
ORIGEN_HOJA = null;
|
||||||
|
FOCO_EN_HOJA = false;
|
||||||
|
var act = document.activeElement;
|
||||||
|
/* Safari no enfoca los botones al hacer clic: el foco cae en body */
|
||||||
|
var focoDentro = !!hoja && (hoja.contains(act) || (movido && (!act || act === document.body)));
|
||||||
if (hoja && hoja.parentNode) hoja.parentNode.removeChild(hoja);
|
if (hoja && hoja.parentNode) hoja.parentNode.removeChild(hoja);
|
||||||
HOJA_ID = "";
|
HOJA_ID = "";
|
||||||
|
if (!focoDentro) return;
|
||||||
|
/* Devolver el foco: al origen si sigue en el DOM; si hubo re-render, al
|
||||||
|
mismo control en la fila nueva del mensaje; si no, a la conversación. */
|
||||||
|
var destino = origen && origen.isConnected ? origen : null;
|
||||||
|
if (!destino && idHoja) {
|
||||||
|
var filaNueva = filaDe({ id: idHoja });
|
||||||
|
if (filaNueva) destino = filaNueva.querySelector("[data-ver-todas], [data-flujo]:not([data-btn]):not([data-card])");
|
||||||
|
}
|
||||||
|
if (!destino) destino = byId("chat-cuerpo");
|
||||||
|
if (destino) { try { destino.focus({ preventScroll: true }); } catch (e) { destino.focus(); } }
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -128,9 +128,32 @@
|
|||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
Historial deshacer / rehacer
|
Historial deshacer / rehacer
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
/* Cada instantánea guarda también qué demo guardada estaba en uso
|
||||||
|
(«wam-demo-activa»): al deshacer o rehacer se repone, para que
|
||||||
|
«Actualizar» nunca escriba el contenido de otra demo en su hueco. */
|
||||||
|
function instantanea() {
|
||||||
|
return { estado: clonar(WAM.estado), activa: lsGet(CLAVE_ACTIVA) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function reponerActiva(id) {
|
||||||
|
if (id) lsSet(CLAVE_ACTIVA, id); else lsDel(CLAVE_ACTIVA);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Un cambio nuevo deja obsoletos los avisos con acción («Deshacer» /
|
||||||
|
«Rehacer»): su botón ya no desharía lo que anuncian. */
|
||||||
|
function cerrarAvisosConAccion() {
|
||||||
|
var ts = document.querySelectorAll("#ed-toasts .ed-toast");
|
||||||
|
for (var i = 0; i < ts.length; i++) {
|
||||||
|
if (!ts[i].querySelector(".ed-toast-accion")) continue;
|
||||||
|
if (typeof ts[i].__cerrar === "function") ts[i].__cerrar();
|
||||||
|
else if (ts[i].parentNode) ts[i].parentNode.removeChild(ts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WAM.puntoDeshacer = function () {
|
WAM.puntoDeshacer = function () {
|
||||||
if (!WAM.estado) return;
|
if (!WAM.estado) return;
|
||||||
pilaDeshacer.push(clonar(WAM.estado));
|
cerrarAvisosConAccion();
|
||||||
|
pilaDeshacer.push(instantanea());
|
||||||
if (pilaDeshacer.length > LIMITE_HISTORIAL) pilaDeshacer.shift();
|
if (pilaDeshacer.length > LIMITE_HISTORIAL) pilaDeshacer.shift();
|
||||||
pilaRehacer = [];
|
pilaRehacer = [];
|
||||||
pintarBotonesHistorial();
|
pintarBotonesHistorial();
|
||||||
@@ -147,8 +170,10 @@
|
|||||||
|
|
||||||
WAM.deshacer = function () {
|
WAM.deshacer = function () {
|
||||||
if (!pilaDeshacer.length) { toast("No hay nada que deshacer"); return false; }
|
if (!pilaDeshacer.length) { toast("No hay nada que deshacer"); return false; }
|
||||||
pilaRehacer.push(clonar(WAM.estado));
|
var previo = pilaDeshacer.pop();
|
||||||
sustituirEstado(conTemaActual(pilaDeshacer.pop()));
|
pilaRehacer.push(instantanea());
|
||||||
|
sustituirEstado(conTemaActual(previo.estado));
|
||||||
|
reponerActiva(previo.activa);
|
||||||
pintarBotonesHistorial();
|
pintarBotonesHistorial();
|
||||||
toast("Cambio deshecho", { accion: { texto: "Rehacer", fn: WAM.rehacer } });
|
toast("Cambio deshecho", { accion: { texto: "Rehacer", fn: WAM.rehacer } });
|
||||||
return true;
|
return true;
|
||||||
@@ -156,8 +181,10 @@
|
|||||||
|
|
||||||
WAM.rehacer = function () {
|
WAM.rehacer = function () {
|
||||||
if (!pilaRehacer.length) { toast("No hay nada que rehacer"); return false; }
|
if (!pilaRehacer.length) { toast("No hay nada que rehacer"); return false; }
|
||||||
pilaDeshacer.push(clonar(WAM.estado));
|
var siguiente = pilaRehacer.pop();
|
||||||
sustituirEstado(conTemaActual(pilaRehacer.pop()));
|
pilaDeshacer.push(instantanea());
|
||||||
|
sustituirEstado(conTemaActual(siguiente.estado));
|
||||||
|
reponerActiva(siguiente.activa);
|
||||||
pintarBotonesHistorial();
|
pintarBotonesHistorial();
|
||||||
toast("Cambio rehecho");
|
toast("Cambio rehecho");
|
||||||
return true;
|
return true;
|
||||||
@@ -372,6 +399,18 @@
|
|||||||
return entrada;
|
return entrada;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ¿El estado actual cambió de cuenta o de número de mensajes respecto a
|
||||||
|
lo guardado en el hueco «id»? */
|
||||||
|
function cambioSustancial(id) {
|
||||||
|
var guardada = null;
|
||||||
|
try { guardada = JSON.parse(lsGet(PREFIJO_DEMO + id) || "null"); } catch (e) { guardada = null; }
|
||||||
|
if (!guardada || typeof guardada !== "object") return false;
|
||||||
|
var nomG = (guardada.cuenta && guardada.cuenta.nombre) || "";
|
||||||
|
var nomA = (WAM.estado && WAM.estado.cuenta && WAM.estado.cuenta.nombre) || "";
|
||||||
|
var nG = Array.isArray(guardada.mensajes) ? guardada.mensajes.length : 0;
|
||||||
|
return nomG !== nomA || nG !== nMensajes();
|
||||||
|
}
|
||||||
|
|
||||||
function cargarDemo(id) {
|
function cargarDemo(id) {
|
||||||
var bruto = lsGet(PREFIJO_DEMO + id);
|
var bruto = lsGet(PREFIJO_DEMO + id);
|
||||||
var datos = null;
|
var datos = null;
|
||||||
@@ -499,11 +538,21 @@
|
|||||||
var bGuardar = boton(activa ? "Actualizar" : "Guardar la actual", "wam-dlg-primario", function () {
|
var bGuardar = boton(activa ? "Actualizar" : "Guardar la actual", "wam-dlg-primario", function () {
|
||||||
var nombre = String(inputNombre.value || "").trim() || nombreCuenta || "Mi demo";
|
var nombre = String(inputNombre.value || "").trim() || nombreCuenta || "Mi demo";
|
||||||
var misma = activa && activa.nombre === nombre;
|
var misma = activa && activa.nombre === nombre;
|
||||||
|
// Defensa: si lo que se va a escribir sobre una demo existente tiene
|
||||||
|
// otra cuenta u otro número de mensajes, se pide un segundo clic.
|
||||||
|
if (misma && bGuardar.getAttribute("data-confirmar") !== "1" && cambioSustancial(activa.id)) {
|
||||||
|
bGuardar.setAttribute("data-confirmar", "1");
|
||||||
|
bGuardar.textContent = "¿Sobrescribir «" + activa.nombre + "»?";
|
||||||
|
toast("El contenido actual no se parece al de «" + activa.nombre + "» (otra cuenta u otro número de mensajes). Pulsa otra vez para sobrescribirla o cambia el nombre para guardarla como nueva.", { ms: 7000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bGuardar.removeAttribute("data-confirmar");
|
||||||
var e = guardarDemoComo(nombre, misma ? activa.id : null);
|
var e = guardarDemoComo(nombre, misma ? activa.id : null);
|
||||||
if (e) { toast("Demo «" + e.nombre + "» guardada"); pintarLista(); activa = demoActiva(); }
|
if (e) { toast("Demo «" + e.nombre + "» guardada"); pintarLista(); activa = demoActiva(); }
|
||||||
});
|
});
|
||||||
inputNombre.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); bGuardar.click(); } });
|
inputNombre.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); bGuardar.click(); } });
|
||||||
inputNombre.addEventListener("input", function () {
|
inputNombre.addEventListener("input", function () {
|
||||||
|
bGuardar.removeAttribute("data-confirmar");
|
||||||
bGuardar.textContent = (activa && activa.nombre === String(inputNombre.value).trim()) ? "Actualizar" : "Guardar como nueva";
|
bGuardar.textContent = (activa && activa.nombre === String(inputNombre.value).trim()) ? "Actualizar" : "Guardar como nueva";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
// Regresiones: deshacer/rehacer con demos guardadas, avisos «Deshacer»
|
||||||
|
// obsoletos, hoja «Todas las opciones» con URLs bloqueadas y foco en las hojas.
|
||||||
|
import { test, expect, estado, filaChat, cargarEstado, rapido } from "./fixtures.mjs";
|
||||||
|
|
||||||
|
async function nombreCuenta(page, v) {
|
||||||
|
await page.evaluate(() => { const d = document.getElementById("bloque-cuenta"); if (d) d.open = true; });
|
||||||
|
await page.locator("#cuenta-nombre").fill(v);
|
||||||
|
await expect.poll(async () => (await estado(page)).cuenta.nombre).toBe(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activaLS = (page) => page.evaluate(() => localStorage.getItem("wam-demo-activa"));
|
||||||
|
|
||||||
|
async function guardarComo(page, nombre) {
|
||||||
|
await page.locator("#btn-demos").click();
|
||||||
|
const dlg = page.locator("#dlg-demos");
|
||||||
|
await dlg.locator("#dlg-demo-nombre").fill(nombre);
|
||||||
|
await dlg.getByRole("button", { name: /Guardar/ }).click();
|
||||||
|
await expect(dlg.locator(".wam-demo-nombre", { hasText: new RegExp("^" + nombre + "$") })).toBeVisible();
|
||||||
|
await dlg.locator(".wam-dlg-pie").getByRole("button", { name: "Cerrar" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("Deshacer y demos guardadas", () => {
|
||||||
|
test("deshacer / rehacer reponen la demo activa y «Actualizar» no pisa otra demo", async ({ app }) => {
|
||||||
|
await guardarComo(app, "A");
|
||||||
|
const idA = await activaLS(app);
|
||||||
|
// cargar un escenario y guardarlo como B
|
||||||
|
await app.locator("#btn-demos").click();
|
||||||
|
await app.locator("#dlg-demos .wam-esc").first().click();
|
||||||
|
await nombreCuenta(app, "Cuenta B");
|
||||||
|
await guardarComo(app, "B");
|
||||||
|
const idB = await activaLS(app);
|
||||||
|
expect(idB).not.toBe(idA);
|
||||||
|
// abrir A
|
||||||
|
await app.locator("#btn-demos").click();
|
||||||
|
await app.locator("#dlg-demos .wam-demo").filter({ has: app.locator(".wam-demo-nombre", { hasText: /^A$/ }) })
|
||||||
|
.getByRole("button", { name: "Abrir" }).click();
|
||||||
|
expect(await activaLS(app)).toBe(idA);
|
||||||
|
// deshacer: vuelve el contenido de B y también la demo activa B
|
||||||
|
await app.locator("#btn-deshacer").click();
|
||||||
|
expect((await estado(app)).cuenta.nombre).toBe("Cuenta B");
|
||||||
|
expect(await activaLS(app)).toBe(idB);
|
||||||
|
await app.locator("#btn-demos").click();
|
||||||
|
await expect(app.locator("#dlg-demo-nombre")).toHaveValue("B");
|
||||||
|
await app.locator("#dlg-demos .wam-dlg-pie").getByRole("button", { name: "Cerrar" }).click();
|
||||||
|
// rehacer: vuelve A
|
||||||
|
await app.locator("#btn-rehacer").click();
|
||||||
|
expect(await activaLS(app)).toBe(idA);
|
||||||
|
// deshacer un escenario (que borra la clave activa) la repone
|
||||||
|
await app.locator("#btn-demos").click();
|
||||||
|
await app.locator("#dlg-demos .wam-esc").first().click();
|
||||||
|
expect(await activaLS(app)).toBeNull();
|
||||||
|
await app.locator("#btn-deshacer").click();
|
||||||
|
expect(await activaLS(app)).toBe(idA);
|
||||||
|
// el hueco de A sigue siendo A
|
||||||
|
const slotA = await app.evaluate((id) => JSON.parse(localStorage.getItem("wam-demo-" + id) || "null"), idA);
|
||||||
|
expect(slotA && slotA.cuenta.nombre).not.toBe("Cuenta B");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("«Actualizar» pide un segundo clic si cambió la cuenta", async ({ app }) => {
|
||||||
|
await guardarComo(app, "A");
|
||||||
|
const idA = await activaLS(app);
|
||||||
|
const nombreOriginal = (await estado(app)).cuenta.nombre;
|
||||||
|
await nombreCuenta(app, "Otra cuenta");
|
||||||
|
await app.locator("#btn-demos").click();
|
||||||
|
const bGuardar = app.locator("#dlg-demos .wam-dlg-primario");
|
||||||
|
await expect(bGuardar).toHaveText("Actualizar");
|
||||||
|
await bGuardar.click();
|
||||||
|
await expect(bGuardar).toHaveText(/Sobrescribir/);
|
||||||
|
let slot = await app.evaluate((id) => JSON.parse(localStorage.getItem("wam-demo-" + id)), idA);
|
||||||
|
expect(slot.cuenta.nombre).toBe(nombreOriginal);
|
||||||
|
await bGuardar.click();
|
||||||
|
slot = await app.evaluate((id) => JSON.parse(localStorage.getItem("wam-demo-" + id)), idA);
|
||||||
|
expect(slot.cuenta.nombre).toBe("Otra cuenta");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("el «Deshacer» de un aviso desaparece cuando hay otro cambio", async ({ app }) => {
|
||||||
|
const n = (await estado(app)).mensajes.length;
|
||||||
|
await app.evaluate(() => WAM.borrarMensaje(0));
|
||||||
|
const accion = app.locator("#ed-toasts .ed-toast-accion");
|
||||||
|
await expect(accion).toHaveCount(1);
|
||||||
|
await nombreCuenta(app, "Nombre nuevo");
|
||||||
|
// sin esperar: el aviso caduca solo a los 6 s y enmascararía el fallo
|
||||||
|
expect(await accion.count()).toBe(0);
|
||||||
|
expect((await estado(app)).mensajes.length).toBe(n - 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PLANTILLA = [
|
||||||
|
{
|
||||||
|
id: "m1", dir: "in", tipo: "template", hora: "10:00", body: "Elige",
|
||||||
|
botones: [
|
||||||
|
{ texto: "Uno" }, { texto: "Dos" },
|
||||||
|
{ texto: "Bloqueado", url: "javascript:alert(1)" },
|
||||||
|
{ texto: "Relativa", url: "/menu" },
|
||||||
|
{ texto: "Cinco" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "m2", dir: "in", tipo: "list", hora: "10:01", body: "Menú", boton: "Ver menú",
|
||||||
|
secciones: [{ titulo: "S", filas: [{ id: "f1", titulo: "Fila 1" }, { id: "f2", titulo: "Fila 2" }] }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.describe("Hojas inferiores", () => {
|
||||||
|
test.beforeEach(async ({ app }) => {
|
||||||
|
await rapido(app);
|
||||||
|
await cargarEstado(app, { mensajes: PLANTILLA });
|
||||||
|
await expect(filaChat(app, "m1")).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("«Todas las opciones» pinta inertes los botones con URL bloqueada o no válida", async ({ app }) => {
|
||||||
|
const fila = filaChat(app, "m1");
|
||||||
|
const inertesBurbuja = await fila.locator(".qr-btn-inactiva").allTextContents();
|
||||||
|
expect(inertesBurbuja.map((t) => t.trim())).toEqual(["Bloqueado", "Relativa"]);
|
||||||
|
await fila.locator("[data-ver-todas]").click();
|
||||||
|
const hoja = app.locator("#sheet");
|
||||||
|
const inertesHoja = await hoja.locator('.sheet-fila.qr-btn-inactiva[aria-disabled="true"]').allTextContents();
|
||||||
|
expect(inertesHoja.map((t) => t.trim())).toEqual(inertesBurbuja.map((t) => t.trim()));
|
||||||
|
await expect(hoja.locator("[data-opcion]")).toHaveCount(3);
|
||||||
|
await expect(hoja.locator('[data-opcion="2"], [data-opcion="3"]')).toHaveCount(0);
|
||||||
|
await expect(hoja.locator(".sheet-fila")).toHaveCount(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("foco: la hoja lo recibe, lo retiene con Tab y lo devuelve al cerrar", async ({ app }) => {
|
||||||
|
const toggle = filaChat(app, "m1").locator("[data-ver-todas]");
|
||||||
|
await toggle.focus();
|
||||||
|
await app.keyboard.press("Enter");
|
||||||
|
await expect(app.locator("#sheet")).toHaveCount(1);
|
||||||
|
const enHoja = () => app.evaluate(() => !!document.activeElement.closest("#sheet"));
|
||||||
|
expect(await enHoja()).toBe(true);
|
||||||
|
for (let k = 0; k < 6; k++) {
|
||||||
|
await app.keyboard.press("Tab");
|
||||||
|
expect(await enHoja()).toBe(true);
|
||||||
|
}
|
||||||
|
await app.keyboard.press("Shift+Tab");
|
||||||
|
expect(await enHoja()).toBe(true);
|
||||||
|
await app.keyboard.press("Escape");
|
||||||
|
await expect(app.locator("#sheet")).toHaveCount(0);
|
||||||
|
expect(await app.evaluate(() => document.activeElement.hasAttribute("data-ver-todas"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("foco: la lista «Ver opciones» enfoca su primera fila", async ({ app }) => {
|
||||||
|
const abridor = filaChat(app, "m2").locator("[data-flujo]:not([data-btn]):not([data-card])").first();
|
||||||
|
await abridor.focus();
|
||||||
|
await app.keyboard.press("Enter");
|
||||||
|
await expect(app.locator("#sheet")).toHaveCount(1);
|
||||||
|
expect(await app.evaluate(() => document.activeElement.getAttribute("data-fila"))).toBe("0");
|
||||||
|
await app.locator("#sheet .sheet-cerrar").click();
|
||||||
|
await expect(app.locator("#sheet")).toHaveCount(0);
|
||||||
|
expect(await app.evaluate(() => document.activeElement.closest('[data-id="m2"]') !== null)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user