10 KiB
QA: Media page, part 1 (browse and manage)
Scope note (informational)
SPA only. The media API already exists (slice A), so there is no HTTP-level Go test in this slice
and no test/ file. The Go suite is run only to confirm nothing regressed (task test).
Slice B2 (upload, multi-select, bulk bar, Duplicates content, merge, drag and drop, rate limit) is specified in features/media_upload_selection_duplicates.feature and its .qa.md.
Toast ids: "media-url-copied-{id}", "media-renamed-{id}", "media-deleted-{id}", "media-not-found-{id}".
Automated (Vitest + Vue Test Utils, task test:web)
Mock useApi's request (or the global fetch it wraps) per case for GET /v1/media/,
GET /v1/media/{id}/usage, GET /v1/media/?hash=, PATCH /v1/media/{id} and
DELETE /v1/media/{id}, plus navigator.clipboard.writeText and navigator.onLine. No real
backend. Mount MediaPage inside DashboardShell with a verified session and a test router at
/media. Sample data is the 12-image fixture in the .feature Background (same ids, filenames,
sizes, dimensions, used_in, uploaders and dates as the board's script), with counts { all: 12,
unused: 2, duplicates: 2 }. Use fake timers for the 150 ms skeleton delay and the 120 ms search
debounce.
Logic and copy:
- Formatting (pure functions) - size: 8192 bytes to "8 KB", 880640 to "860 KB", 4089446 to
"3.9 MB"; dimensions "4032 × 3024"; null width and height to "Vector"; date "12 Sep 2026"
from
created_at; type from content_type (image/jpeg "JPEG", image/png "PNG", image/webp "WEBP", image/svg+xml "SVG"); accessible name for used_in 2, 1 and 0 ("used in 2 articles", "used in 1 article", "unused"). - Query string mapping - Examples-driven from the .feature: each URL produces the exact request query and control state; unknown values fall back to defaults; changing a control rewrites the URL and drops default values.
- Default - 12 cards in fixture order, tabs "All 12" "Unused 2" "Duplicates 2", the Duplicates badge only when duplicates > 0, "Select all" and "12 images" in the Select all row, breadcrumb "Media".
- Badges and accessible names - the 5 Examples rows (badges and aria-label / accessible name).
- Grid, few images - 3 items; tabs "All 3" "Unused 1" "Duplicates" without a badge.
- List view - desktop: table with the 7 column headers and 12 rows, the Used in cell underlined count or Unused badge; mobile: card list with "3.9 MB · 4032 × 3024 · JPEG" and "Marta Alves · 12 Sep 2026"; clicking the Used in count opens the panel.
- Duplicates tab - covered by the B2 spec.
- Loading - unresolved GET: nothing before 150 ms; after 150 ms the skeleton (10 cards, 6 below md) and aria-busy; resolve; cards replace it; same on retry and on search, tab and sort change, in list view too.
- Empty library - zero items and counts 0 with no q: toolbar and tabs absent, copy exact, "Upload an image" enabled, the footnote "PNG, JPEG, WEBP, SVG · max 10 MB each".
- No results - unused + "wall": title
No images match "wall", text "Nothing matches in Unused. Check the spelling, or clear the filters."; "Clear filters" resets q and filter and restores the cards. - Keyboard - each card is a button; Enter opens the panel; Space toggles selection and opens nothing; arrow keys move focus (left, right, up, down by one row, stopping at the edges); Escape closes the panel and focus returns to the card.
- Detail panel, in use - usage called once for id 1, title, actions, metadata rows, article rows with links to /article/{id}/edit and Online / Draft badges, footnote text.
- Detail panel, unused - no usage request; "Not used in any article" and the safe-to-delete text.
- Detail panel, duplicate - hash query used; alert title "Identical to 2 other images" and "floorboards-copy.jpg and IMG_2208.jpg have the same content." (the image itself is excluded); the "Review duplicates" action is enabled and selects the Duplicates tab.
- Copy URL - clipboard called with
{origin}{url}; toast id "media-url-copied-1", icon circle-check, title "URL copied", text "Paste it wherever you need the image address.". - Rename dialog - title, description, field "north-wall" with ".jpg" suffix, help text, Cancel then Rename.
- Rename validation - "", "---", "!!!": error "Enter a filename." on submit and on blur, no PATCH.
- Rename saving - PATCH /v1/media/1 body { filename: "north-wall-01" } (no extension); the field and buttons disabled, "Renaming…", dialog not dismissible by Escape or backdrop.
- Rename failed - 500 and network rejection: alert copy exact, text kept, buttons enabled.
- Rename done - 200: panel title, card and row updated in place, order unchanged; toast id
"media-renamed-1", title "Image renamed", text
It's now "north-wall-01.jpg". The URL is the same. - Delete, unused and Delete, in use - dialog titles, descriptions with the exact quotes and counts, the article list with badges only in the in-use variant, "Delete image" danger.
- Delete in progress - DELETE /v1/media/8; buttons disabled; "Deleting…"; not dismissible.
- Delete failed - 500 and network rejection: alert copy exact, item untouched.
- Delete done - 204: dialog and panel closed, card removed, focus on the next card (heading
when last), toast id "media-deleted-8", title "Image deleted", text
"window-frame.jpg" is gone from your library.; a background list refresh without skeleton follows and updates the counts. - 404 Examples-driven - DELETE, PATCH, usage: everything closes, the item is removed, toast id "media-not-found-8", icon info, title "That image no longer exists", text "It was deleted somewhere else, so we removed it from the library.".
- List failed - 500 and network rejection: alert copy exact, toolbar visible; "Try again" re-requests, shows skeleton, then the list.
- Offline - onLine false: banner copy exact, Upload, Rename and Delete disabled, Copy URL and panel still work; back online restores them.
- 401 / 403 - handled by useApi (redirect target strings asserted); no local media copy.
- Sidebar and drawer - Media between Articles and Users with the images icon, active on /media, in the desktop sidebar and the mobile drawer; the route has meta.auth "verified" and meta.title "Media" (routerGuard spec extended).
- Header - title, subtitle, and the Upload button (also the mobile pinned 48 px variant).
Visual (task visual:check -- media, desktop and mobile, light and dark)
Reference: docs/design/screens/media.md and source/MediaFrame.dc.html (group "Media" in the
back-office board). Add each setup to web/visual/states.ts first, mocking /v1/media/,
/v1/media/{id}/usage and /v1/media/?hash= from the board's own script: the 12 images (ids,
filenames, sizes, dimensions, used in, uploaders, absolute dates), and the article rows for
the panels ("How we price a bathroom renovation" Online, "Winter checklist for timber windows"
Draft for north-wall.jpg). None of the B1 states depends on the current date (all dates are
absolute), so none is skip: 'clock-sensitive'.
The board draws its thumbnails as placeholder tiles (a striped tone fill with an image icon, the RenewCMS mark on the SVG, a checkerboard behind PNG and SVG), not photographs. The mocked image URLs must render the same way, or the 10 % gate fails for a reason unrelated to the code.
Harness note: the B2 states now have setups and no longer carry skip: 'pending'.
Setups for this slice (name in screens/media.md, then what must be drawn):
- Grid, many images: 12 cards, tabs 12 / 2 / 2, Select all row showing "12 images" only.
- Grid, few images: ids 1, 4, 8; checkerboard behind the SVG.
- List view: table at desktop, card list at mobile.
- Loading: skeleton cards (10 desktop, 6 mobile), toolbar visible.
- Empty library, first run: no toolbar, "No images yet" panel and the footnote.
- No results: unused + "wall".
- Hover, focus and keyboard: hover on id 2, focus ring on id 3 (the checkbox reveal is covered by B2).
- Detail panel: in use (id 1), unused (id 8), duplicate (id 12).
- Copy URL: id 1 panel plus the toast.
- Rename: dialog, saving, validation, failed, done (id 1, "north-wall-01").
- Delete: unused (id 8), in use (id 1), in progress (id 1), failed (id 8), done (grid without id 8 plus toast).
- Image already gone (404): grid without id 8 plus the info toast.
- List failed to load: alert with "Try again".
- Offline: banner and disabled Upload.
The B2 states are listed in features/media_upload_selection_duplicates.qa.md.
Manual checks screenshots cannot cover:
- Keyboard and focus order: Tab visits the search, tabs, sort, toggle, then one stop per card; arrow keys move across the grid; Enter opens the panel; focus is trapped in the panel and in dialogs, starts on Cancel in dialogs, and returns to the card on close; after a delete focus lands on the next card.
- Hover, motion and reduced motion: card hover shadow, skeleton pulse, panel and dialog transitions stop under reduced motion.
- Screen reader: the card's accessible name reads correctly.
Manual verification
- Real round trips against a running server (
task dev): list with real counts, sorting, search, usage, rename (extension kept, URL unchanged), delete, a concurrent 404. Vitest only mocksuseApi. - Real image rendering: the thumbnails and previews load from the real
urland the checkerboard shows behind real transparent PNG and SVG files. - Real clipboard write in a real browser.
- Toast placement and stacking on a live app.
- Counts after a delete against a live server: the background refresh gives the true Unused and Duplicates counts.
- A library above 25 images: amended by
features/media_pagination_screen.feature. B1 used to requestlimit=100and ignorenext_cursor; the page now requestspage=1&size=25(or the stored size) and shows a paginator, so every image is reachable.