feat: image variants settings screens

This commit is contained in:
fsylvain committed 2026-10-08 13:57:41 +02:00
1 parent f1d56ef50e
commit f567866161
42 files changed
+7192 -11

No files matched your search

+156
View File
@@ -0,0 +1,156 @@
# QA: Image variants settings screens
## Implementation note for coder (informational, not a QA assertion)
The API is built and covered by features/image-variants.qa.md: this slice has no Go change and no HTTP-level
section. The auth requirement "admin" does not exist in router meta (AuthRequirement is
public | session | verified | any); the role check lives in VariantsPage via the session role from
GET /v1/auth/me. Plain-text API errors are read through ApiError.body; never display the raw body.
Sizes: the new formatter is not formatSize. Every new string has an English and a
French entry in web/src/i18n/messages.ts.
## Automated (Vitest + Vue Test Utils, `task test:web`)
Mock useApi's `request` per case (GET /v1/image-variants, POST /v1/image-variants, PUT, PATCH,
DELETE, POST .../purge); no backend. Mount VariantsPage inside DashboardShell with a resolved verified
session (id 1, username "marta", role "owner") and a test router at /settings/media/variants.
Default fixture = the Background's 6 variants in API order. Mock `matchMedia` for the 640 px
breakpoint; run layout cases at desktop and below-640. Use fake timers for the 150 ms skeleton delay.
Mock navigator.clipboard. No test freezes the system date (this screen shows no date).
1. **Access, Examples-driven** — session without role; role "owner"; role "admin"; list answering
ApiError(403). Assert the Forbidden copy and "Back to Home" (navigates to /home) and that no list
request is sent when the role is missing; assert the page header for owner and admin.
2. **Header and tabs** — title, description, "Add variant", tabs "Variants" (active) and "Cache"
(aria-disabled, click does nothing), breadcrumb "Settings", "Media", "Variants" and
mobile title "Variants".
3. **Loaded, desktop** — column labels, the 6 rows in order with every cell of the table, "—" for
print, "Disabled" for print, muted text on the disabled row, footer "6 variants · 1,284 generated
files · 212 MB", the reference card.
4. **Cell formats, Examples-driven** — the four dimension/quality/format cases.
5. **Loaded, mobile** — 6 cards, the spec lines (`1200×630 · cover · WebP · q82`, print without a
quality part), "244 generated · 84 MB", the footer after the cards, the reference card.
6. **Footer** — Default sums; a one-variant list "1 variant · 1 generated file · 18 KB"; an
after-delete recompute "5 variants · 1,040 generated files · 128 MB".
7. **Loading** — unresolved GET: nothing at 149 ms; at 150 ms the region with aria-busy and the label
"Loading variants", 5 skeleton rows on desktop / 4 cards below 640, no text; a response at 100 ms never
draws skeletons.
8. **Empty** — copy, the two template cards, "Add variant", no reference card; each template opens
the form with its values (thumb 400×300 cover WebP 80; hero 1200×630 cover WebP 82).
9. **Error, Examples-driven** — ApiError(500) and NetworkError: alert copy, "Try again", no close
button, no table; Try again re-sends GET and renders the Default fixture.
10. **Status switch** — accessible name "Enable variant hero"; toggling hero sends `PATCH
/v1/image-variants/hero {"enabled": false}`, switch disabled while in flight, row shows Disabled,
no toast; print on sends `{"enabled": true}`; failures 403/404/500 revert the switch and raise
the toast copy of the table.
11. **Row menu** — items Edit/Purge cache/Delete in order, ArrowDown/Up and Enter, Escape closes and
returns focus to the ellipsis button; Edit opens the edit form, the others open their dialog.
12. **Purge** — dialog copy for hero ("Purge 244 files", "84 MB"); Cancel/Escape send nothing; confirm
sends `POST /v1/image-variants/hero/purge`, closes the dialog, row dimmed with "Purging…" and
disabled controls while others stay active; resolve `{purged_files: 244, purged_bytes: 88080384}`
-> row 0 files, status Enabled, toast "Purged 244 files from “hero”"; failures 404/500 -> row restored + toast.
13. **Delete** — dialog copy; confirm sends `DELETE /v1/image-variants/hero`; spinner, Cancel
disabled, Escape and backdrop blocked; 204 -> row removed, toast `Variant “hero” deleted`, footer
recompute; deleting the last row shows Empty; failures 404 (reloads the list) / 500 -> toast.
14. **Reference card** — the 4 rows with badge words and icons, the exact JSON block text, the note;
absent in Loading/Empty/Error/Forbidden; one column below 640.
15. **Slide-over open/close** — desktop slide-over vs mobile full page (back arrow, no x), titles
"Add variant" / "Edit variant: hero", focus on Name, pristine close without dialog, dirty close
via Cancel/x/back/Escape opens "Discard changes?" with the right text, Keep editing / Discard
changes behaviours, focus return to the opener.
16. **Create: pristine** — all labels, placeholders, helps, defaults (Fit Cover, Format Original,
Quality 80, Generate off, Enabled on), "Add variant" disabled, overlay "Set a width or height to
see a preview.".
17. **Fit help, Examples-driven** — three helps.
18. **Field errors, Examples-driven** — each name/width/height/quality row of the feature; blur and
submit; revalidation on input; boundary values accepted; several errors at once: no request.
19. **No dimension** — copy under Width, error border on Height, default help hidden, no request.
20. **PNG** — Quality hidden, body without "quality", quality restored after switching back.
21. **Quality slider/number sync**.
22. **Create request** — exact POST body for the hero values; the `201` flow inserts the row in
name order without a second GET, footer recomputed, toast `Variant “hero” added`.
23. **Saving** — fields disabled, "Saving…", Escape/x/backdrop blocked.
24. **409** — inline message with the name, values kept, no alert and no toast, cleared on edit.
25. **Save error, Examples-driven** — ApiError(500) and NetworkError: alert copy; Try again repeats the
request; values kept.
26. **Other statuses, Examples-driven** — 400, 403, 404 toasts.
27. **422, Examples-driven** — the four server sentences and their inline/alert mapping.
28. **Edit: pristine** — locked Name with help, stored values, warning alert text with 244, Save
disabled.
29. **Edit: dirty and save** — quality 82 -> 70: estimate "about 71 KB", Save enabled, PUT body
without "name", 200 with purged_files 244 -> row updated in place, toast "Variant “hero” saved. 244 files
purged."; purged_files 0 -> "Variant “hero” saved.".
30. **Preview layout** — heading, sample text, labels, hint, chips "Original" and the name, handle size
(44 px class on mobile).
31. **ImageCompare keyboard, Examples-driven** — role/type range, min 0 max 100 step 1, value 50,
aria-label "Compare original and hero", aria-valuetext "50% hero"; every key row of the feature,
including clamping; valuetext and clip-path/divider position follow the value; pointer input sets it.
32. **ImageCompare empty** — no dimensions: no variant layer, divider or handle; overlay text.
33. **Output size and estimate, Examples-driven** — the six size rows and the four estimate rows;
monotonic quality; never above 3.9 MB.
34. **URL** — URL text with and without a name, Copy disabled without a name, click writes the
displayed URL, label "Copied" with fake timers and back to "Copy", a rejected clipboard keeps "Copy".
35. **French** — `setLocale('fr')`; every key used on the page exists in messages.ts (a catalogue
test) and the page renders without missing keys.
36. **Vocabulary** — no rendered text contains "thumbnail preset", "version" or "rendition".
## Visual (task visual:check -- image-variants, desktop and mobile, light and dark)
Reference: docs/design/screens/image-variants.md, board "Image variants" in
`docs/design/source/RenewCMS Screens Image variants.dc.html`, 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. All states below use the board's sample data; none is clock-sensitive,
so none is skipped.
Sample data (from the board): the 6 variants of the Background (thumb 412 files 21 MB, card 318/58 MB,
hero 244/84 MB, square 140/19 MB, og 96/9 MB, print 74/21 MB), board row order as listed in
VariantsFrame (thumb, card, hero, square, og, print); (the board orders by creation, the API by name;
the mock must return the board order for the visual capture only). Session name "Marta Alves".
Preview sample: north-wall.jpg, 4032 × 3024, 3.9 MB.
- [ ] 1a Loaded (desktop table, mobile cards, reference card on desktop)
- [ ] 1b Loading
- [ ] 1c Empty with the two templates
- [ ] 1d Error loading
- [ ] 1e Forbidden (session without role, no nav item current)
- [ ] 1f Row menu (hero)
- [ ] 1g Row action in progress (hero "Purging…")
- [ ] 1h Purge confirm
- [ ] 1i Delete confirm
- [ ] 1j Delete in progress
- [ ] 1k Request reference (mobile scrolled 900 px; desktop shows it under the table)
- [ ] 2a Create: pristine
- [ ] 2b Create: dirty (hero, slider focused, mobile scrolled to the preview, estimate 96 KB)
- [ ] 2c Field errors (name "Hero banner", width 9000, height 630, quality 140)
- [ ] 2d No dimension (name "banner")
- [ ] 2e Name already taken (name "thumb", 400×300, estimate 18 KB)
- [ ] 2f Saving
- [ ] 2g Save error
- [ ] 2h Edit: pristine
- [ ] 2i Edit: dirty (quality 70, estimate 71 KB)
- [ ] 2j Unsaved changes (discard dialog over 2i)
- [ ] 2k PNG: no quality (estimate 310 KB)
- [ ] Layout, taken from the board frame (VariantsFrame.dc.html), not components.md: table grid
`minmax(0,1.1fr) 104px 76px 72px 68px 92px 92px 132px 44px`, 36 px header, 48 px rows; cards 12 px
padding with the name+switch line, the spec line, then count+menu; slide-over 960 px with a
440 px form column; compare box 3:2, chips 8 px from the top corners, handle 32 px (44 px on
mobile) centred on a 2 px divider; footer 64 px (72 px on mobile).
- [ ] Not drawn, no visual state: toggle failures, the purge/delete failure toasts, the 409-free
status toasts, the 422 mapping, the clipboard states "Copied" (add only if the board draws it: it
does not, so Vitest only), singular footer.
- [ ] Not covered by screenshots: keyboard and focus order (Tab reaches Add variant, tabs, each row's
switch and ellipsis; the slide-over focus starts on Name and is trapped; Escape rules), slider
keys by hand, hover/active, motion (slide-over, dialogs, skeleton pulse stopped under reduced
motion).
## Manual verification
- **Round trips against `task dev`**: create, edit (PUT with a purge), toggle, purge and delete a real
variant; confirm rows, counts and sizes after a reload match the API (needs generated files from a
real `/static/uploadedImages/<storedName>?v=<name>` request).
- **Role**: sign in as an owner and as an admin; confirm both reach the page. A session without a role
is not reproducible live (only owner/admin exist); covered by Vitest and the visual state.
- **Real clipboard** in a secure context (localhost): Copy then paste.
- **French**: switch the language and read every state, the dialogs and the toasts.
- **Real public URL**: paste the copied URL on a stored image and confirm it serves the variant.