Files
bookhoard/tests/ui/COVERAGE.md
T
John O'Keefe fef79b1836 feat(devices): pair-a-device card replaces the pending registration list
The Devices page's Pending Device Registrations section (Approve/Reject
cards broadcast to every account) is replaced by an always-visible Pair
a Device field: type the 6-digit code shown on the registering e-reader,
a templ-rendered confirm card shows what will be bound, and confirming
claims the device to the signed-in account.

Rendering ownership (AGENTS.md): the confirm card and every lookup
outcome are templ partials (PairingLookupResult) swapped by htmx from a
new SSR route POST /devices/pairing/lookup — the server decides what UI
looks like, no JSON-built HTML. Errors render as content because htmx
only swaps 2xx bodies (the route is web-only; the JSON API keeps real
status codes), and one delegated htmx:responseError listener in
device-management.ts toasts the non-swap failure path.

The claim confirm button posts straight to /api/devices/claim; success
comes back as HX-Refresh (same pattern as token regeneration), so the
page reloads into the paired state. No new TS module is needed — plain
htmx attributes drive the whole flow.

Ride-alongs: the Add New Device modal loses the device-type select and
the kobo manual form (its post-register UX depended on the broadcast
list and silently produced an unclaimable registration; native Kobo
sync is still server-side-only and will get its own pairing UX when it
lands) — the modal is now a single Connect-a-KOReader-Device guide with
the pairing steps. PendingRegistrationData, convertPending, and the
pending plumbing in the page handler are gone with the section.
ui-controls.md glossary and tests/ui COVERAGE.md updated in the same
commit as the control rename, per the drift gates.
2026-11-02 16:11:15 -05:00

7.9 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 🔲 🔲
24 Devices: Pair a Device — code lookup renders templ confirm card, claim binds + page refresh; wrong/expired code renders inline error partial (not a toast-only path) 🔲 🔲 (registration invisible by design)

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).