diff --git a/tests/ui/COVERAGE.md b/tests/ui/COVERAGE.md index f27e7fb6..a0e1d74a 100644 --- a/tests/ui/COVERAGE.md +++ b/tests/ui/COVERAGE.md @@ -40,6 +40,15 @@ Gate: every line below executes for BOTH personas unless marked - Row 23 chains by page navigation — assert the URL becomes the next episode's reader and progress carries over. +## 2026-11-02 — promise-API confirm probe + +`promise_confirm_probe.spec.ts` (no env gating): the confirmDialog() +promise path (purge archived, remove account, …) must wire the global +modal's buttons even when no hx-confirm element fired first — the +Purge All Archived dialog opened with dead Cancel/Confirm otherwise +(user report; confirm.ts now wires at init + on every confirmDialog). +The probe intercepts the purge POST, so nothing is deleted. + ## 2026-10-30 — round-3 regression probes (live-server, env-gated) Two specs added against a RUNNING dev server (skip unless diff --git a/tests/ui/promise_confirm_probe.spec.ts b/tests/ui/promise_confirm_probe.spec.ts new file mode 100644 index 00000000..d6b7413a --- /dev/null +++ b/tests/ui/promise_confirm_probe.spec.ts @@ -0,0 +1,53 @@ +// Regression probe (2026-11-02): the CONFIRM PROMISE API path must wire +// the global modal's buttons too. purgeArchivedItems() (admin archived +// page) opens confirmDialog() directly — no hx-confirm element fires — +// and both buttons were dead whenever no hx-confirm had been clicked +// first: the modal opened and Cancel/Confirm did nothing (user report). +// The purge request itself is INTERCEPTED here — nothing is deleted. +import { test, expect } from "@playwright/test"; + +const BASE = process.env.BH_BASE ?? "http://localhost:8765"; + +test("confirmDialog promise path wires Cancel/Confirm", async ({ page }) => { + await page.addInitScript(() => { + const t = new URLSearchParams(window.location.search).get("token"); + if (t) localStorage.setItem("token", t); + }); + // testuser login via endpoint, then seed cookie+localStorage like the + // other probes (rls_sweep pattern, kept inline to stay one file). + const res = await page.request.post(`${BASE}/api/auth/login`, { + data: { login: "testuser", password: "Test@Pass123!" }, + }); + const { access_token: token } = (await res.json()) as { access_token?: string }; + expect(token).toBeTruthy(); + await page.context().addCookies([{ name: "token", value: token!, url: BASE }]); + await page.addInitScript((t) => localStorage.setItem("token", t), token!); + + // The purge POST is intercepted: Cancel/Confirm behavior is asserted + // without ever deleting the dev data. + let purgeRequested = false; + await page.route("**/api/media-items/purge-archived", (route) => { + purgeRequested = true; + route.fulfill({ status: 200, contentType: "application/json", body: '{"purged":0}' }); + }); + + await page.goto(`${BASE}/admin/archived`); + await expect(page.locator("#confirm-modal")).toBeAttached(); + + // Fresh page: no hx-confirm has fired, so pre-fix the buttons were + // unwired (data-wired absent) and Cancel did nothing. + await page.getByRole("button", { name: "Purge All Archived" }).click(); + const modal = page.locator("#confirm-modal"); + await expect(modal).toBeVisible(); + await expect(page.locator("#confirm-modal-cancel")).toHaveAttribute("data-wired", "1"); + + await page.locator("#confirm-modal-cancel").click(); + await expect(modal).toBeHidden(); + + // Confirm path: modal closes AND the guarded request issues. + await page.getByRole("button", { name: "Purge All Archived" }).click(); + await expect(modal).toBeVisible(); + await page.locator("#confirm-modal-accept").click(); + await expect(modal).toBeHidden(); + expect(purgeRequested).toBe(true); +}); diff --git a/web/src/confirm.ts b/web/src/confirm.ts index 58dda364..9f6149e6 100644 --- a/web/src/confirm.ts +++ b/web/src/confirm.ts @@ -25,6 +25,7 @@ function hide(ok: boolean): void { * Safe to call from any module (import this instead of window.confirm). */ export function confirmDialog(message: string): Promise { + wireButtons(); const { overlay, message: messageEl } = modalElements(); if (!overlay || !messageEl) { // Header missing (bare layouts): degrade to the native dialog @@ -38,7 +39,13 @@ export function confirmDialog(message: string): Promise { }); } -// Wire the static buttons once. +// Wire the static buttons once. Called at module init AND from both +// entry paths below (idempotent via data-wired): the htmx:confirm +// interceptor and the confirmDialog promise API. Wiring only on the +// htmx path left the promise path (purge archived, remove account, ...) +// with dead Cancel/Confirm buttons whenever no hx-confirm element had +// been clicked first — the modal opened and nothing could close it +// (user report: Purge Archived, 2026-11-02). function wireButtons(): void { const { overlay } = modalElements(); const accept = document.getElementById("confirm-modal-accept"); @@ -60,6 +67,10 @@ function wireButtons(): void { } } +// The header (and its confirm modal) render before deferred modules run, +// so the elements are present at import time. +wireButtons(); + // htmx:confirm interceptor: every element with hx-confirm gets the // modal instead of the browser dialog; confirming issues the request. // (Canonical htmx pattern — see htmx docs, "confirming with a modal".)