Files
RenewCMS/features/media_browse.qa.md

153 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.