8.2 KiB
QA: Image variants on the Media page
Implementation note for coder (informational, not a QA assertion)
The API is built by features/image_variants_per_image_api.feature. This slice has no Go change. The role check lives in the panel via the session role from GET /v1/auth/me (there is no "admin" auth requirement in router meta). Plain-text API errors are read through ApiError.body and mapped by status; never display the raw body. The percent-saved helper lives in one module under web/src/lib/variants/ and rounds half away from zero, then clamps the magnitude to 99 while the file is non-empty. New strings need English and French entries in web/src/i18n/messages.ts. A mock of GET /v1/media/{id}/variants returns the API order (name); only the visual mock uses the board order.
Automated (Vitest + Vue Test Utils, task test:web)
Mock useApi's request per case (GET /v1/media/, GET /v1/media/{id}/variants, POST /v1/media/{id}/variants[/{name}], DELETE /v1/media/{id}/variants/{name}, DELETE /v1/media/{id}, GET /v1/activity). Reuse web/src/views/__tests__/mediaTestKit.ts and variantsTestKit.ts; add variant_count and variant_size to the 12-image fixture of the Background. Mount MediaView inside DashboardShell with a resolved verified session (id 1, role "owner") and a test router at /media. Mock matchMedia for the 640 px and md breakpoints; run layout cases at desktop and below. Fake timers for the 150 ms skeleton and for nothing else. Mock navigator.clipboard. No test freezes the system date and none asserts a relative date.
- Badge, Examples-driven - the five card rows, the position after Used in / Duplicate, singular "1 variant", no badge at 0, the unchanged accessible name, no badge in list view.
- Filter control - placement desktop and mobile, selected style, each segment sends
variants=with|withoutwithlimit=100and shows the skeleton after 150 ms, "All" removes the parameter, the four URL rows (?variants=allandzzzdropped and the URL cleaned), combination with?filter=unused. - Sort - the five options in order, the new label and its mobile label,
sort=total_sizein the URL and request, the stale?sort=totaldropped. - Meta line - the four rows while sorted by total size, and the usual line under Newest.
- Empty filter - 3e copy, the toolbar stays, "Show all images" removes
?variantsand restores the default cards; noqset is the only case this panel shows. - Delete dialogs - the note for 4 and for 1 variant and its absence at 0, in-use title and description unchanged, the bulk sum "9 generated variants (670 KB)" for the three selected images and no note when the sum is 0, no extra request, the unchanged pending, failure and 404 flows, the background refresh after success.
- Tabs - present for owner and admin, absent for another role with no variants request,
GET /v1/media/{id}/variantssent once per open panel on first selection of the tab, Details body unchanged. - Loading and load error, Examples-driven - 149 and 150 ms, aria-busy and label, five skeleton rows, 500 and network alert and Try again.
- Partly generated - Original block copy, summary "4 of 5 generated", row order and every cell of the generated and not generated rows.
- Saving percent, Examples-driven - the seven size rows against 4089446 bytes, plus "-100%" never shown.
- Nothing generated, no variants defined, SVG - copy, link target
/settings/media/variants, no list, no Generate all. - Row actions - icon buttons with tooltips and accessible names on desktop, labelled buttons below 640 px, the actions per state, Generate all disabled when all five are generated.
- Generate now - request, Generating row and summary and disabled button (resolve later with fake timers off, using a deferred promise), success toast id and title, 403, 404 (tab reloaded; second 404 runs the image-gone flow), 500 and network rows return the row to Not generated.
- Generate all - request, rows in flight, success toast ("Generated 4 variants", singular "Generated 1 variant"), partial failure rows with causes and Retry, the two single-failure causes (413 and 415) with their toast text.
- Failed state is local - closing and reopening shows Not generated; Retry re-sends the per-name POST.
- Copy URL - absolute url with
?v=, for a not generated row too, toast id, and a rejected clipboard shows no success toast. - Purge - request, response
{purged_files: 1, purged_bytes: 18432}, row, summary and the toast title; 403, 404, 500. - Preview - copy of both panes and the hint, 880 px / bottom sheet, focus trap and focus return, images are the variant url and the original url.
- Activity sentences - the two sentences, the failed forms, the detail lines for every params row including the singular forms and the two-cause join, no second line for other actions, French catalogue test (every new key present in messages.ts).
- Accessibility - icon plus word for every status, live region text, 44 px classes on mobile, vocabulary scan (no "thumbnail preset", "version", "rendition").
Visual (task visual:check -- image-variants , desktop and mobile, light and dark)
Reference: docs/design/screens/image-variants.md, board "Image variants", exact values in docs/design/source/VariantsFrame.dc.html and AppFrame.dc.html. Add each state to web/visual/states.ts with the board's data. The gate is hard: every non-skipped capture within 10 %, both viewports, both themes. None of these states is clock-sensitive, so none is skipped. Board sample data: the 12 library images with their tone, sizes, used counts, variant counts and variant sizes (IM in VariantsFrame), the five enabled variants in board order thumb, card, hero, square, og (the mock returns the board order for the capture only), north-wall.jpg 4032 x 3024 3.9 MB, row sizes 18 KB, 64 KB, 96 KB, 82 KB, 142 KB. Session name "Marta Alves".
- 3a Grid with badges
- 3b Sort menu open, new option selected
- 3c With variants, sorted by total size (ids 1, 7, 9, 12, 5, 11, 2, 3, 10)
- 3d Without variants (ids 4, 6, 8)
- 3e Nothing to show (With variants, empty)
- 3f Delete confirm for north-wall.jpg with the "Also removes 4 generated variants (260 KB)" note
- 4a Partly generated (thumb, card, hero, square generated, og not)
- 4b Nothing generated yet
- 4c Generating (thumb generated, card and hero generating, square and og not) - reached by pressing Generate all against a mock whose POST stays pending
- 4d Generation failed (card "Unsupported source format", hero "Image too large", others generated) - reached by a Generate all that resolves with those failures
- 4e No variants defined
- 4f SVG source (logo-renew.svg)
- 4h Preview dialog on "thumb"
- Layout from the board frame, not components.md: the segmented control (30 px desktop, 44 px mobile segments, accented border), the badge fill and icon, the panel tabs (40 px / 44 px), the Original block (72 x 54 thumbnail), the row paddings (10 px 12 px) and gaps (4 px desktop, 8 px mobile), the 28 px square icon buttons, the preview pane heights (300 px / 180 px).
- 4g (animated GIF) has no state: the source cannot be uploaded.
- Not drawn, no visual state: load error, toasts, the 403/404/500 paths, bulk delete note, the Activity sentences and detail lines (Vitest only).
- Not covered by screenshots: keyboard and focus order (Tab reaches the tab strip, Generate all, each row's buttons in order, the dialogs trap focus, Escape closes Preview), tooltips on hover, motion.
Manual verification
- Round trip against
task devwith real images: upload an image, open its Variants tab, press Generate now on one row and Generate all, check the sizes and "saves N%" against the files, Purge a row, Preview at actual size, Copy URL then open the pasted URL. - Library: filter With variants / Without and sort by total size against the real cache; delete an image with variants and confirm the note matches the files removed.
- Real clipboard in a secure context (localhost).
- French: switch the language and read the tab, the dialogs, the toasts and the Activity rows.
- Activity: read the two new rows and their detail lines in the real Activity screen against section 8.