# 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 bottom sheet (grabber, x; full-page back arrow superseded by features/variants_visual_conformance.feature), 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 (mobile sheet footer: see features/variants_visual_conformance.feature). - [ ] 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/?v=` 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.