153 lines
10 KiB
Markdown
153 lines
10 KiB
Markdown
# 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:
|
||
|
||
1. **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").
|
||
2. **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.
|
||
3. **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".
|
||
4. **Badges and accessible names** - the 5 Examples rows (badges and aria-label / accessible name).
|
||
5. **Grid, few images** - 3 items; tabs "All 3" "Unused 1" "Duplicates" without a badge.
|
||
6. **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.
|
||
7. **Duplicates tab** - covered by the B2 spec.
|
||
8. **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.
|
||
9. **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".
|
||
10. **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.
|
||
11. **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.
|
||
12. **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.
|
||
13. **Detail panel, unused** - no usage request; "Not used in any article" and the safe-to-delete
|
||
text.
|
||
14. **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.
|
||
15. **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.".
|
||
16. **Rename dialog** - title, description, field "north-wall" with ".jpg" suffix, help text,
|
||
Cancel then Rename.
|
||
17. **Rename validation** - "", "---", "!!!": error "Enter a filename." on submit and on blur,
|
||
no PATCH.
|
||
18. **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.
|
||
19. **Rename failed** - 500 and network rejection: alert copy exact, text kept, buttons enabled.
|
||
20. **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.`
|
||
21. **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.
|
||
22. **Delete in progress** - DELETE /v1/media/8; buttons disabled; "Deleting…"; not dismissible.
|
||
23. **Delete failed** - 500 and network rejection: alert copy exact, item untouched.
|
||
24. **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.
|
||
25. **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.".
|
||
26. **List failed** - 500 and network rejection: alert copy exact, toolbar visible; "Try again"
|
||
re-requests, shows skeleton, then the list.
|
||
27. **Offline** - onLine false: banner copy exact, Upload, Rename and Delete disabled, Copy URL
|
||
and panel still work; back online restores them.
|
||
28. **401 / 403** - handled by useApi (redirect target strings asserted); no local media copy.
|
||
29. **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).
|
||
30. **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
|
||
mocks `useApi`.
|
||
- **Real image rendering**: the thumbnails and previews load from the real `url` and 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
|
||
request `limit=100` and ignore `next_cursor`; the page now requests `page=1&size=25` (or the stored
|
||
size) and shows a paginator, so every image is reachable.
|