Files
bookhoard/tests/ui/COVERAGE.md
T
John O'Keefe 2165829d0e
Release / build-and-push (push) Successful in 3m29s
fix(ux): confirm modal's Cancel/Confirm were dead on the promise-API path
User report: the Purge Archived dialog ('Permanently delete all
archived items?...') opened, and Cancel did nothing — and Confirm
would have been equally dead, leaving the page stuck behind the
overlay until a reload.

Root cause: confirm.ts wired the modal's buttons ONLY inside the
htmx:confirm interceptor. confirmDialog() — the promise API used by
purgeArchivedItems, remove-account, and other TS call sites — never
wired them. So whenever a page's first confirm interaction came from
the promise path (no hx-confirm element clicked first), both buttons
had no listeners. The 2026-10-30 confirm_probe spec only exercised the
hx-confirm path, which is why it survived.

Fix: wireButtons() now runs at module init (the header renders before
deferred modules) and inside confirmDialog() — both idempotent via the
existing data-wired guard.

Pinned by tests/ui/promise_confirm_probe.spec.ts (no env gating): the
purge POST is route-intercepted, so the probe asserts Cancel closes
the modal and Confirm issues the guarded request without deleting any
dev data. hx-confirm path re-pinned green (confirm_probe with BH_FEED).
Verified manually at 390x844: open -> cancelWired=true -> Cancel
closes -> Confirm fires the intercepted request.
2026-11-02 11:24:55 -05:00

7.7 KiB

UI sweep coverage (plan §7.5)

Gate: every line below executes for BOTH personas unless marked "admin-only". Status: ✅ swept+green · 🔲 pending (future sweep runs).

# Page / control admin restricted
1 Login: bad password rejected; good login lands authed; logout ✅ ✅
2 Dashboard: sections render (Recently Added / Not Started) ✅ ✅ (hidden lib absent)
3 Dashboard: hidden library visibility (Audiobooks) ✅ visible ✅ absent
4 Search: results scoped (/api/media-items/search) ✅ ✅ no leaks
5 Media list: no hidden items in payloads ✅ ✅
6 Library browse grid + pagination 🔲 🔲
7 Library create / edit / delete (admin) 🔲 n/a
8 Admin visibility toggle roundtrip (hide → restricted loses → unhide) 🔲 🔲
9 Media detail: edit metadata / archive / delete 🔲 n/a
10 Reader: open, page turn, TOC, bookmark, highlight, note, settings 🔲 🔲
11 Collections: CRUD, add/remove items, smart-rule editor 🔲 🔲
12 Series: list/detail 🔲 🔲
13 Stats screens + range filters 🔲 🔲
14 Admin: users CRUD, visibility matrix, settings toggles, processing issues 🔲 n/a
15 Upload: EPUB generated at runtime, upload → appears → delete 🔲 n/a
16 Auth edges: restricted → admin URL; anonymous → media URL 🔲 ✅ (detail/404)
17 OPDS / KOReader API-covered (invariant test) API-covered
18 Podcasts: page renders (empty state before any feed) 🔲 🔲
19 Podcasts: add-feed modal → feed page (member flow) 🔲 🔲
20 Podcasts: feed detail — episode list lifecycle (download/queue/failed+retry states inline), Download All, gap banner 🔲 🔲
21 Podcasts: cross-user invisibility — own feed only (RLS §20 surfaces) 🔲 🔲
22 Podcasts: order selector re-renders episode list; serial suggestion shown only for serial/audiobook feeds 🔲 🔲
23 Podcasts: audio reader "Ep N/M" + prev/next episode; chain-advance to next episode 🔲 🔲

Podcast-specific sweep notes (plan task 1.10)

  • The download lifecycle needs a fetchable enclosure: sweep with a local feed server fixture (the integration tests use swapFeedServer; the Playwright run needs the same trick) — otherwise episodes stay pending and rows 20's queue/failed states can't render.
  • Row 21 rides TestPodcastRLSSurfaces (API level) until the sweep lands; the page-level check is the 404 render for a foreign feed id.
  • 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 BH_FEED=<feed uuid> is set for the first):

  • confirm_probe.spec.ts — every hx-confirm must open the GLOBAL confirm modal, never a native dialog. Pins the htmx-2 detail-shape fix (detail.question, not detail.element) and the modal's presence on signed-in pages (it used to live inside SidebarSignIn).
  • add_order_probe.spec.ts — the add-modal "Download order" select reveals when "Download all episodes now" is checked (CSS sibling selector; the checkbox must stay a sibling of .podcast-add-order).

2026-10-30 — episode collections (plan item 2.3) — sweep candidates

New controls the Playwright sweep should cover once it lands:

  • Collection picker modal: Books/Episodes tab pair — episodes tab loads ?scope=episodes, selection persists across tab switches and htmx swaps (shared bookPicker store).
  • Collection detail: episode rows (play link carries ?queue=collection:<id>; chain hops keep the param), "Not downloaded" badge, "Unavailable / Remove entry" hint row for collected-but-unsubscribed feeds (All Libraries view), header "Play as Sequence" button.
  • Player (collection mode): order dropdown + reverse button ABSENT (feed-mode only); "Ep N/M" reflects collection order; auto-advance follows the collection chain, and back-to-detail exits to the collection page.
  • API-level pins live in TestPodcastEpisodeCollections (7 subtests incl. the family-lens auditor case) + TestPodcastPlayerOrderControl.

2026-10-30 — browser sweep executed (manual, OpenChamber panel)

All controls above plus the item-3 builder/editor surfaces were driven in a real browser against the dev stack. Outcomes + reusable gotchas:

  • Fixed during the sweep: templ has no continue STATEMENT (codegen emits it as literal text — the library switcher's podcast exclusion never worked); CSP attribute grammar (no backticks, no ?., no if-statements in Alpine attrs); rules editor double-encoded payloads; store hook must use the IMPORTED Alpine (window.Alpine?.store silently no-ops). Pins: TestLibrarySwitcherExcludesPodcasts, templates CSP source scan, TestPodcastEpisodeCollections/TestEpisodeCollectionsFilterTree.
  • Panel limitation for future automated specs: selectOption-equivalent via this panel sets the value WITHOUT a change event — htmx change-triggers (player order dropdown, library switcher) must be driven with Playwright-native interactions, not value assignment.
  • Episode flows confirmed: picker Episodes tab → Add Selected → cards with queue=collection: play links → chain hops keep the param; order controls appear ONLY in feed mode (player + feed page share the persisted override).

2026-10-31 — round 4: user-reported control defects → knob matrix (automated)

The panel sweep's blind spot, proven by the user's own two minutes: DOM-text snapshots show HIDDEN menus' text — "the field menu opened" was an illusion; the menus never VISIBLY opened, the op menu rendered zero items, closeCollectionModal removed the hidden confirm dialog instead of the modal, and the theme buttons died silently (selectTheme(opt.Name) — templ gotcha #1, since the CSP migration).

Now automated: knob_matrix.spec.ts (6 specs — theme picker PUTs, pencil-glyph vs card body at real coordinates, builder field/op menus with toBeVisible + non-empty items + connective both directions, the Smart→Episodes create-modal seed, picker modal ladder+geometry via elementFromPoint/boundingBox, chain-order select persistence). Run: cd tests/ui && BH_FEED=<feed uuid> npx playwright test --workers=1 — --workers=1 is load-bearing: the login endpoint rate-limits and every suite shares testuser, so a parallel worker's login revokes another's mid-run (symptom: session has expired pages).

Standing rules for every sweep from now on:

  • Menus/modals assert VISIBILITY (toBeVisible / computed display / screenshot), never DOM text; every toggle click has an asserted observable outcome (request fired, class flipped, value changed).
  • Geometry asserts where geometry is the bug (z-ladder via elementFromPoint, sheet within viewport via boundingBox).
  • Zero new console errors per interaction; the only tolerated noise is the two cancelled-transition shapes swallowed by main.ts's gate — new noise gets fixed at the source, never filtered.
  • API-mutating tests seed localStorage token + selectedLibrary=all (the mutators read the bearer from localStorage; library-scoped views correctly hide episodes).