feat: image variants settings screens
This commit is contained in:
1 parent
f1d56ef50e
commit
f567866161
42 files changed
+7192
-11
No files matched your search
@@ -0,0 +1,690 @@
|
||||
Feature: Image variants settings screens, list and add/edit slide-over
|
||||
|
||||
As an owner or admin of a RenewCMS site
|
||||
I want to list, enable, disable, purge, delete, add and edit image variants from Settings → Media
|
||||
So that I can manage the named sizes articles request with ?v=<name> without leaving the back office
|
||||
|
||||
Background:
|
||||
Given the spec of record is docs/design/screens/image-variants.md sections 1 and 2 and its "Back-office API errors" table, the board docs/design/source/RenewCMS Screens Image variants.dc.html drawn by VariantsFrame.dc.html, and docs/design/routes.md
|
||||
And this slice is SPA only: no Go change, and the API is the one of features/image-variants.feature
|
||||
And it excludes the Cache tab content, the Media library additions, the image detail Variants tab and the editor picker (sections 3 to 6 of the design)
|
||||
And /settings/media/variants is a verified route rendered as VariantsPage inside DashboardShell, with the sidebar item "Settings" current
|
||||
And the desktop breadcrumb is "Settings › Media › Variants" and the mobile top bar title is "Variants"
|
||||
And the page uses Nuxt UI components only and design tokens, with no raw hex value and no arbitrary Tailwind colour
|
||||
And every new English string has a French entry in web/src/i18n/messages.ts
|
||||
And the list comes from GET /v1/image-variants once per navigation, and items are used in the order the API returns (name ascending)
|
||||
And the Default fixture holds six variants, in API order, with cache_size in bytes (1 MB = 1048576):
|
||||
| name | width | height | fit | format | quality | generated_files | cache_size | enabled |
|
||||
| card | 800 | null | inside | webp | 80 | 318 | 58 MB | true |
|
||||
| hero | 1200 | 630 | cover | webp | 82 | 244 | 84 MB | true |
|
||||
| og | 1200 | 630 | cover | jpeg | 80 | 96 | 9 MB | true |
|
||||
| print | null | 2400 | inside | png | null | 74 | 21 MB | false |
|
||||
| square | 600 | 600 | cover | jpeg | 85 | 140 | 19 MB | true |
|
||||
| thumb | 400 | 300 | cover | webp | 80 | 412 | 21 MB | true |
|
||||
And the session user has role "owner" or "admin" from GET /v1/auth/me unless a scenario says otherwise
|
||||
And plain-text API errors are mapped by status, never shown raw
|
||||
|
||||
Rule: Only admins and owners see the page
|
||||
|
||||
Scenario Outline: A session whose role is not owner or admin gets the Forbidden state
|
||||
Given the session has <session>
|
||||
When the user opens /settings/media/variants
|
||||
Then the sidebar shows no current item
|
||||
And the page shows the empty block with a lock icon, the title "You don't have access" and the text "Only admins can manage image variants. Ask an admin if you need one changed."
|
||||
And it offers one button "Back to Home" with an arrow-left icon
|
||||
And no page header, no tabs, no table, no cards and no reference card are drawn
|
||||
And <request>
|
||||
|
||||
Examples:
|
||||
| session | request |
|
||||
| no role | no request to GET /v1/image-variants is sent |
|
||||
| role "owner" but the API answers 403 to the list | the request was sent and its 403 is handled here |
|
||||
|
||||
Scenario: Back to Home leaves the page
|
||||
Given the Forbidden state is shown
|
||||
When the user activates "Back to Home"
|
||||
Then the route is /home
|
||||
|
||||
Scenario Outline: An owner or an admin sees the list
|
||||
Given the session role is "<role>"
|
||||
When the user opens /settings/media/variants
|
||||
Then GET /v1/image-variants is sent once and the page header is shown
|
||||
|
||||
Examples:
|
||||
| role |
|
||||
| owner |
|
||||
| admin |
|
||||
|
||||
Rule: Header and tabs
|
||||
|
||||
Scenario: The page header and the tabs
|
||||
When the Default fixture is loaded
|
||||
Then the title is "Image variants"
|
||||
And the description is "Named sizes and formats generated from your original images. Originals are never changed."
|
||||
And a primary button "Add variant" with a plus icon is shown, right of the title on desktop and below the description on mobile
|
||||
And two tabs follow the header: "Variants" (icon layers, active, medium weight, 2 px primary underline) and "Cache" (icon database, toned text), the Cache tab is aria-disabled="true", activating it changes nothing and no route exists for it
|
||||
|
||||
Rule: Loaded, table on desktop and cards on mobile
|
||||
|
||||
Scenario: Desktop table
|
||||
Given the viewport is 640 px wide or more
|
||||
When the Default fixture is loaded
|
||||
Then a table has the columns "Name", "Dimensions", "Fit", "Format", "Quality", "Generated", "Cache size", "Status" and an unlabeled actions column
|
||||
And the grid is minmax(0,1.1fr) 104px 76px 72px 68px 92px 92px 132px 44px, the header is 36 px high on --ui-bg-elevated and rows are at least 48 px high with a top border
|
||||
And the rows are, in order, with cells name | dimensions | fit | format | quality | generated | cache size | status:
|
||||
| card | 800 × auto | inside | WebP | 80 | 318 | 58 MB | Enabled |
|
||||
| hero | 1200×630 | cover | WebP | 82 | 244 | 84 MB | Enabled |
|
||||
| og | 1200×630 | cover | JPEG | 80 | 96 | 9 MB | Enabled |
|
||||
| print | auto × 2400| inside | PNG | — | 74 | 21 MB | Disabled |
|
||||
| square | 600×600 | cover | JPEG | 85 | 140 | 19 MB | Enabled |
|
||||
| thumb | 400×300 | cover | WebP | 80 | 412 | 21 MB | Enabled |
|
||||
And the name and dimensions cells are IBM Plex Mono, the name 13 px medium and the dimensions 12 px
|
||||
And the row of a disabled variant has muted text
|
||||
And the footer under the table reads "6 variants · 1,284 generated files · 212 MB"
|
||||
And below the footer the Request reference card is visible
|
||||
|
||||
Scenario Outline: Dimensions, quality and format cells
|
||||
Given a variant with width <width>, height <height>, format <format> and quality <quality>
|
||||
Then the Dimensions cell reads "<dimensions>", the Format cell reads "<label>" and the Quality cell reads "<qualityCell>"
|
||||
|
||||
Examples:
|
||||
| width | height | format | quality | dimensions | label | qualityCell |
|
||||
| 400 | 300 | webp | 80 | 400×300 | WebP | 80 |
|
||||
| 800 | null | jpeg | 80 | 800 × auto | JPEG | 80 |
|
||||
| null | 2400 | png | null | auto × 2400 | PNG | — |
|
||||
| 600 | 600 | original | 82 | 600×600 | Original | 82 |
|
||||
|
||||
Scenario: Mobile cards
|
||||
Given the viewport is below 640 px
|
||||
When the Default fixture is loaded
|
||||
Then each variant is a card with 12 px padding and a 1 px border, stacked with 8 px gaps
|
||||
And the first line of the card "hero" is the name in IBM Plex Mono 14 px medium with the status switch at its right
|
||||
And the second line is the mono 12 px spec line "1200×630 · cover · WebP · q82"
|
||||
And the third line is "244 generated · 84 MB" with the row menu button at its right
|
||||
And the card "print" spec line is "auto × 2400 · inside · PNG" with no quality part
|
||||
And the footer text "6 variants · 1,284 generated files · 212 MB" follows the last card
|
||||
And the Request reference card follows the footer, in one column
|
||||
|
||||
Scenario: Singular footer
|
||||
Given the list holds only "thumb" with 1 generated file of 18 KB
|
||||
Then the footer reads "1 variant · 1 generated file · 18 KB"
|
||||
|
||||
Scenario: Footer sums are computed from the list
|
||||
Given the Default fixture
|
||||
Then the footer file count is the sum of generated_files and the size is the sum of cache_size
|
||||
|
||||
Rule: Loading
|
||||
|
||||
Scenario: Skeletons appear only after 150 ms
|
||||
Given GET /v1/image-variants has not answered
|
||||
Then nothing is drawn in the content area at 149 ms
|
||||
And at 150 ms the header and tabs stay, and a region announces "Loading variants" with aria-busy="true"
|
||||
And on desktop it holds a 36 px header bar and 5 skeleton rows of 48 px with six bars
|
||||
And below 640 px it holds 4 skeleton cards with three bars of 40 %, 70 % and 55 % width
|
||||
And no text is drawn in the skeleton
|
||||
|
||||
Scenario: A fast answer never flashes skeletons
|
||||
Given GET /v1/image-variants answers before 150 ms
|
||||
Then no skeleton is ever drawn
|
||||
|
||||
Rule: Empty
|
||||
|
||||
Scenario: No variants
|
||||
Given GET /v1/image-variants answers {"items": []}
|
||||
Then the header and tabs are shown
|
||||
And the empty block has a layers icon, the title "No variants yet" and the text "A variant is a named size that an image can be requested in, like ?v=thumb. Start with one of these."
|
||||
And two template cards sit side by side on desktop and stacked on mobile (at least 64 px high):
|
||||
| thumb | 400×300 · cover · WebP | Use template |
|
||||
| hero | 1200×630 · cover · WebP | Use template |
|
||||
And a primary button "Add variant" with a plus icon follows them
|
||||
And no table, footer or Request reference card is drawn
|
||||
|
||||
Scenario Outline: A template opens the form pre-filled
|
||||
Given the empty state is shown
|
||||
When the user activates the template <template>
|
||||
Then the Add variant form opens in create mode with name "<template>", width <width>, height <height>, fit cover, format WebP and quality <quality>
|
||||
And generate on upload is off and enabled is on
|
||||
And Save is enabled
|
||||
|
||||
Examples:
|
||||
| template | width | height | quality |
|
||||
| thumb | 400 | 300 | 80 |
|
||||
| hero | 1200 | 630 | 82 |
|
||||
|
||||
Rule: Error loading
|
||||
|
||||
Scenario Outline: A failed list request
|
||||
Given GET /v1/image-variants fails with <failure>
|
||||
Then the header and tabs are shown
|
||||
And an error alert shows the title "Couldn't load variants", the text "Check your connection and try again." and a button "Try again" with a rotate-cw icon
|
||||
And the alert has no close button and no table, cards, footer or reference card are drawn
|
||||
|
||||
Examples:
|
||||
| failure |
|
||||
| status 500 |
|
||||
| a network failure |
|
||||
|
||||
Scenario: Try again reloads
|
||||
Given the error alert is shown
|
||||
When the user activates "Try again"
|
||||
Then GET /v1/image-variants is sent again and the Default fixture replaces the alert
|
||||
|
||||
Rule: Status switch
|
||||
|
||||
Scenario: Each row has a labelled switch paired with an icon and a word
|
||||
When the Default fixture is loaded
|
||||
Then every row shows a switch (32 × 18 px) and the icon circle-check with "Enabled" or the icon circle-off with "Disabled"
|
||||
And the switch of "hero" has the accessible name "Enable variant hero"
|
||||
|
||||
Scenario Outline: Toggling sends PATCH and updates the row
|
||||
Given the variant "<name>" is <before>
|
||||
When the user toggles its switch
|
||||
Then PATCH /v1/image-variants/<name> is sent with {"enabled": <after>}
|
||||
And while it is in flight the switch is disabled
|
||||
And when it answers 200 the status reads "<label>" and the other fields of the row are unchanged
|
||||
And no purge happens and generated_files and cache_size are those of the response
|
||||
|
||||
Examples:
|
||||
| name | before | after | label |
|
||||
| hero | enabled | false | Disabled |
|
||||
| print | disabled | true | Enabled |
|
||||
|
||||
Scenario Outline: A failed toggle reverts and reports
|
||||
Given the variant "hero" is enabled
|
||||
When the user toggles its switch and the API answers <status>
|
||||
Then the switch and the word return to "Enabled"
|
||||
And a toast shows the title "<title>" and the text "<text>"
|
||||
|
||||
Examples:
|
||||
| status | title | text |
|
||||
| 403 | You don't have access | Only admins can manage image variants. |
|
||||
| 404 | Not found | This variant or image no longer exists. Refresh the page. |
|
||||
| 500 | Something went wrong | The server couldn't finish. Try again. If it keeps happening, check the server log. |
|
||||
|
||||
Rule: Row menu
|
||||
|
||||
Scenario: The ellipsis opens a three-item menu
|
||||
When the user activates the ellipsis button of the row "hero" (accessible name and tooltip "Actions for hero")
|
||||
Then a 176 px menu opens at the right of the row with the items "Edit" (pencil), "Purge cache" (eraser) and "Delete" (trash-2, error colour)
|
||||
And on desktop items are 32 px high and on mobile 44 px high
|
||||
And the ellipsis button shows a primary focus ring while the menu is open
|
||||
|
||||
Scenario: Menu keyboard behaviour
|
||||
Given the menu is open
|
||||
Then the arrow keys move between items, Enter activates the focused item and Escape closes the menu with focus back on the ellipsis button
|
||||
|
||||
Scenario Outline: Menu items
|
||||
Given the menu of "hero" is open
|
||||
When the user chooses "<item>"
|
||||
Then <result>
|
||||
|
||||
Examples:
|
||||
| item | result |
|
||||
| Edit | the add/edit slide-over opens in edit mode for "hero" |
|
||||
| Purge cache | the purge dialog opens |
|
||||
| Delete | the delete dialog opens |
|
||||
|
||||
Rule: Purge cache
|
||||
|
||||
Scenario: The purge dialog
|
||||
When the purge dialog of "hero" opens (hero has 244 files and 84 MB)
|
||||
Then the title is "Purge cache for “hero”?"
|
||||
And the text is "This removes 244 generated files (84 MB). Files are generated again on the next request."
|
||||
And the buttons are "Cancel" and a primary "Purge 244 files"
|
||||
And focus is trapped in the dialog and returns to the ellipsis button on close
|
||||
|
||||
Scenario: Cancel closes without a request
|
||||
When the user activates "Cancel" or presses Escape
|
||||
Then the dialog closes, no request is sent and the row is unchanged
|
||||
|
||||
Scenario: Confirming purges while the row is dimmed
|
||||
When the user activates "Purge 244 files"
|
||||
Then POST /v1/image-variants/hero/purge is sent
|
||||
And the dialog closes
|
||||
And the row "hero" is on --ui-bg-elevated, its switch and ellipsis button are disabled and its status cell reads "Purging…" with a loader-circle icon in the info colour
|
||||
And the other rows stay interactive
|
||||
|
||||
Scenario: Purge succeeds
|
||||
Given the API answers 200 {"purged_files": 244, "purged_bytes": 88080384}
|
||||
Then the row "hero" shows Generated 0 and Cache size 0 KB, its status returns to Enabled and its controls are active
|
||||
And the footer is recomputed with no refetch
|
||||
And a toast reads "Purged 244 files from “hero”" (the count comes from the response)
|
||||
|
||||
Scenario Outline: Purge fails
|
||||
When the purge answers <status>
|
||||
Then the row returns to its previous values and controls
|
||||
And a toast shows "<title>" and "<text>"
|
||||
|
||||
Examples:
|
||||
| status | title | text |
|
||||
| 404 | Not found | This variant or image no longer exists. Refresh the page. |
|
||||
| 500 | Something went wrong | The server couldn't finish. Try again. If it keeps happening, check the server log. |
|
||||
|
||||
Rule: Delete variant
|
||||
|
||||
Scenario: The delete dialog
|
||||
When the delete dialog of "hero" opens
|
||||
Then the title is "Delete variant “hero”?"
|
||||
And the text is "This removes the variant and its 244 generated files (84 MB). Articles that use ?v=hero will start returning 400. This can't be undone."
|
||||
And the buttons are "Cancel" and a danger "Delete variant"
|
||||
|
||||
Scenario: Confirming shows a spinner and blocks dismissal
|
||||
When the user activates "Delete variant"
|
||||
Then DELETE /v1/image-variants/hero is sent
|
||||
And the confirm button shows a loader-circle icon and "Cancel" is disabled
|
||||
And Escape and a backdrop click do not close the dialog
|
||||
|
||||
Scenario: Delete succeeds
|
||||
Given the API answers 204
|
||||
Then the dialog closes, the row "hero" is removed, the footer reads "5 variants · 1,040 generated files · 128 MB"
|
||||
And a toast reads "Variant “hero” deleted"
|
||||
|
||||
Scenario: Deleting the last variant shows the empty state
|
||||
Given only "thumb" exists and DELETE answers 204
|
||||
Then the "No variants yet" empty state replaces the table
|
||||
|
||||
Scenario Outline: Delete fails
|
||||
When the delete answers <status>
|
||||
Then the dialog closes, the row stays [404: the list is reloaded], and a toast shows "<title>" and "<text>"
|
||||
|
||||
Examples:
|
||||
| status | title | text |
|
||||
| 404 | Not found | This variant or image no longer exists. Refresh the page. |
|
||||
| 500 | Something went wrong | The server couldn't finish. Try again. If it keeps happening, check the server log. |
|
||||
|
||||
Rule: Request reference card
|
||||
|
||||
Scenario: The card under the table
|
||||
Given the list is loaded
|
||||
Then a card titled "How requests are handled" shows two columns on desktop (1.3fr / 1fr, 16 px gap) and one on mobile
|
||||
And the left column is a bordered table (rows at least 36 px, 44 px on mobile):
|
||||
| /static/uploadedImages/photo.png?v=thumb | 200 | Image |
|
||||
| Unknown or disabled variant | 400 | Unknown variant |
|
||||
| width, height, format or quality | 400 | Unsupported parameter |
|
||||
| Missing source image | 404 | Not found |
|
||||
And the first cell is mono 12 px
|
||||
And the 200 badge is success-soft with circle-check and the 400 and 404 badges are error-soft with circle-x
|
||||
And the right column has the muted label "Body of a 400 response" over a mono 12 px block reading exactly:
|
||||
"""
|
||||
{
|
||||
"status": 400,
|
||||
"error": "unknown_variant",
|
||||
"message": "Unknown variant",
|
||||
"detail": "banner"
|
||||
}
|
||||
"""
|
||||
And the note "unsupported_parameter replaces unknown_variant when a raw parameter is sent. There is no arbitrary-size mode." closes the card
|
||||
|
||||
Scenario: The card is not drawn when there is no list
|
||||
Given the state is Loading, Empty, Error or Forbidden
|
||||
Then the Request reference card is absent
|
||||
|
||||
Rule: Slide-over opening and closing
|
||||
|
||||
Scenario: Add variant opens the slide-over on desktop
|
||||
Given the viewport is 640 px wide or more
|
||||
When the user activates "Add variant"
|
||||
Then a slide-over 960 px wide opens at the right over an overlay, the list stays behind it
|
||||
And its header is 56 px high with the title "Add variant" and a close button (x icon, accessible name "Close")
|
||||
And its body has the form on the left (440 px column) and the preview on the right on --ui-bg-muted, separated by a border
|
||||
And its footer is 64 px high with "Cancel" and a primary "Add variant", and the status text is empty
|
||||
And focus moves to the Name field
|
||||
|
||||
Scenario: Add variant opens a full page on mobile
|
||||
Given the viewport is below 640 px
|
||||
When the user activates "Add variant"
|
||||
Then a full-screen page opens with a 56 px header holding an arrow-left back button and the title "Add variant", no x button
|
||||
And the form is followed by the Preview section, stacked, separated by a top border
|
||||
And the footer is 72 px high with "Cancel" and "Add variant" sharing the width
|
||||
|
||||
Scenario: Edit opens with the stored values
|
||||
When the user chooses "Edit" on the row "hero"
|
||||
Then the title is "Edit variant: hero"
|
||||
And Name is "hero" in a locked, elevated field with a lock icon and the help "The name can't be changed."
|
||||
And Width 1200, Height 630, Fit Cover, Format WebP, Quality 82, Generate on upload off and Enabled on are shown
|
||||
And the always-visible warning alert reads "Changing dimensions, fit, format or quality purges 244 generated files; they will be regenerated on next request."
|
||||
And "Save changes" is disabled and the footer status is empty
|
||||
|
||||
Scenario: Dismissing a pristine form closes immediately
|
||||
Given a form with no change
|
||||
When the user activates "Cancel", the close button or presses Escape
|
||||
Then the slide-over closes with no dialog and focus returns to the opener
|
||||
|
||||
Scenario: Dismissing a dirty form asks first
|
||||
Given the form is dirty
|
||||
When the user activates "Cancel", the close button, the back arrow or presses Escape
|
||||
Then a dialog opens with the title "Discard changes?" and the text "You have unsaved changes to “hero”. If you close now, they're lost." in edit mode
|
||||
And its buttons are "Keep editing" and a danger "Discard changes"
|
||||
And "Keep editing" returns to the form with its values
|
||||
And "Discard changes" closes the slide-over and discards the values
|
||||
|
||||
Scenario: The discard text names the new variant in create mode
|
||||
Given the form is dirty in create mode with name "banner"
|
||||
When the user closes it
|
||||
Then the text reads "You have unsaved changes to “banner”. If you close now, they're lost."
|
||||
And with an empty name the text reads "You have unsaved changes. If you close now, they're lost."
|
||||
|
||||
Rule: Form states
|
||||
|
||||
Scenario: Create: pristine
|
||||
When Add variant has just opened
|
||||
Then Name is empty and focused (primary border and 1 px primary ring) with the placeholder "e.g. hero" and the help "Lowercase letters, numbers and dashes. Can't be changed later."
|
||||
And Width and Height are empty with the placeholder "auto", the suffix "px" and the help "Set a width, a height, or both. 1–8000 px. Images are never enlarged."
|
||||
And Fit is a horizontal 3-option radio group, Cover selected, with the help "Fills the box and crops what overflows."
|
||||
And Format is a select showing "Original" with the help "Keeps the format of each source image."
|
||||
And Quality shows a slider and a number field at 80 with the help "1–100. Higher is sharper and larger."
|
||||
And the switch "Generate on upload" (help "Creates this variant when an image is uploaded.") is off
|
||||
And the switch "Enabled" (help "Disabled variants return 400 on the public site.") is on
|
||||
And "Add variant" in the footer is disabled
|
||||
And the Preview shows "Set a width or height to see a preview." over the sample
|
||||
|
||||
Scenario Outline: The Fit help follows the selection
|
||||
When Fit <fit> is selected
|
||||
Then the help reads "<help>"
|
||||
|
||||
Examples:
|
||||
| fit | help |
|
||||
| Cover | Fills the box and crops what overflows. |
|
||||
| Contain | Whole image inside the box, padded to the box size. |
|
||||
| Inside | Whole image inside the box, no padding. |
|
||||
|
||||
Scenario: The Name counter
|
||||
When the user types "hero"
|
||||
Then a mono counter "4/32" shows at the right of the label; in edit mode there is no counter
|
||||
|
||||
Scenario: Create: dirty with valid values
|
||||
Given the user typed name "hero", Width 1200, Height 630, Format WebP, Quality 82, Generate on upload on
|
||||
Then "Add variant" is enabled and the footer status reads "Unsaved changes" with a circle-dot icon
|
||||
And the Preview shows the comparison, its chips and the estimate (see Preview rules)
|
||||
|
||||
Scenario Outline: Field errors show on blur and on submit
|
||||
When the user enters <value> in <field> and leaves it
|
||||
Then the field has an error border and the inline message "<message>" with a circle-alert icon
|
||||
And the help text is replaced by the message
|
||||
And once an error shows it is revalidated on every input and clears when the value is valid
|
||||
|
||||
Examples:
|
||||
| field | value | message |
|
||||
| Name | Hero banner | Use lowercase letters, numbers and dashes, 2 to 32 characters. |
|
||||
| Name | a | Use lowercase letters, numbers and dashes, 2 to 32 characters. |
|
||||
| Name | 33 letters | Use lowercase letters, numbers and dashes, 2 to 32 characters. |
|
||||
| Width | 9000 | Enter a value from 1 to 8000. |
|
||||
| Width | 0 | Enter a value from 1 to 8000. |
|
||||
| Height | 8001 | Enter a value from 1 to 8000. |
|
||||
| Quality | 140 | Enter a value from 1 to 100. |
|
||||
| Quality | 0 | Enter a value from 1 to 100. |
|
||||
|
||||
Scenario Outline: Valid boundary values show no error
|
||||
Then <field> <value> shows no error
|
||||
Examples:
|
||||
| field | value |
|
||||
| Name | ab |
|
||||
| Name | a-b-c |
|
||||
| Width | 1 |
|
||||
| Height | 8000 |
|
||||
| Quality | 1 |
|
||||
| Quality | 100 |
|
||||
|
||||
Scenario: Submitting with several errors sends nothing
|
||||
Given name "Hero banner", Width 9000, Height 630, Format WebP, Quality 140
|
||||
When the user activates "Add variant"
|
||||
Then the three errors of the Field errors scenario show (Name format, "Enter a value from 1 to 8000." under Width, "Enter a value from 1 to 100." under Quality)
|
||||
And no request is sent and focus moves to the first invalid field
|
||||
|
||||
Scenario: No dimension
|
||||
Given name "banner", Width and Height both empty, Format WebP
|
||||
When the user activates "Add variant"
|
||||
Then the message "Set a width, a height, or both." shows under Width, and the Height field has an error border without text
|
||||
And the default help under the two fields is hidden
|
||||
And no request is sent
|
||||
|
||||
Scenario: PNG hides Quality
|
||||
When Format PNG is selected
|
||||
Then the Quality section (slider, number field and help) is not rendered
|
||||
And the estimate and output label follow the PNG rules
|
||||
And the POST or PUT body has no "quality" key
|
||||
|
||||
Scenario: Switching back from PNG restores the previous quality
|
||||
Given Quality was 70 before PNG was selected
|
||||
When Format WebP is selected again
|
||||
Then Quality shows 70
|
||||
|
||||
Scenario: The Quality slider and field stay in sync
|
||||
When the slider moves to 60
|
||||
Then the number field reads 60
|
||||
When the number field is set to 90
|
||||
Then the slider fill is at 90 %
|
||||
|
||||
Rule: Saving a new variant
|
||||
|
||||
Scenario: Add sends POST with the form values
|
||||
Given name "hero", Width 1200, Height 630, Fit Cover, Format WebP, Quality 82, Generate on upload on, Enabled on
|
||||
When the user activates "Add variant"
|
||||
Then POST /v1/image-variants is sent with {"name": "hero", "width": 1200, "height": 630, "fit": "cover", "format": "webp", "quality": 82, "generate_on_upload": true, "enabled": true}
|
||||
|
||||
Scenario: An empty Width or Height is sent as null or omitted
|
||||
Given name "card", Width 800, Height empty
|
||||
Then the body has "width": 800 and no positive "height"
|
||||
And an empty dimension is omitted or null in the body, both being valid for the API
|
||||
|
||||
Scenario: While saving, the form is locked
|
||||
Given the POST is in flight
|
||||
Then every field, switch and select is disabled at 60 % opacity
|
||||
And "Cancel" is disabled and the primary button reads "Saving…" with a loader-circle icon
|
||||
And Escape, the close button and a backdrop click do not close the slide-over
|
||||
|
||||
Scenario: A successful create
|
||||
Given the API answers 201 with the variant
|
||||
Then the slide-over closes, the new row is inserted in name order without a refetch with Generated 0 and Cache size 0 KB, the footer is recomputed
|
||||
And a toast reads "Variant “hero” added"
|
||||
And focus returns to "Add variant"
|
||||
|
||||
Scenario: A create from the empty state leaves the empty state
|
||||
Given the list was empty and a create answers 201
|
||||
Then the table shows one row and the footer reads "1 variant · 0 generated files · 0 KB"
|
||||
|
||||
Scenario: Name already taken (409)
|
||||
Given name "thumb" and the API answers 409 with "A variant named “thumb” already exists. Choose another name."
|
||||
Then the slide-over stays open and the Name field has an error border with the message "A variant named “thumb” already exists. Choose another name."
|
||||
And all values are kept, focus is on Name, the form is re-enabled and no alert or toast is shown
|
||||
And editing the name clears the message
|
||||
|
||||
Scenario Outline: Save error
|
||||
Given the API fails with <failure>
|
||||
Then the slide-over stays open with all values kept and the form re-enabled
|
||||
And an error alert at the top of the form shows the title "Couldn't save the variant", the text "Check your connection and try again." and a button "Try again"
|
||||
And "Try again" repeats the same request; the alert disappears when the request starts
|
||||
|
||||
Examples:
|
||||
| failure |
|
||||
| status 500 |
|
||||
| a network failure |
|
||||
|
||||
Scenario Outline: Other statuses on save
|
||||
Given the API answers <status> to the save
|
||||
Then the slide-over stays open with values kept and a toast shows "<title>" and "<text>"
|
||||
|
||||
Examples:
|
||||
| status | title | text |
|
||||
| 400 | Check the request | A value in the request isn't valid. Reload the page and try again. |
|
||||
| 403 | You don't have access | Only admins can manage image variants. |
|
||||
| 404 | Not found | This variant or image no longer exists. Refresh the page. |
|
||||
|
||||
Scenario Outline: A 422 from the server is shown inline
|
||||
Given the API answers 422 with the plain text "<server text>"
|
||||
Then the message "<inline>" shows under <field>
|
||||
|
||||
Examples:
|
||||
| server text | inline | field |
|
||||
| Use lowercase letters, numbers and dashes, 2 to 32 characters. | Use lowercase letters, numbers and dashes, 2 to 32 characters. | Name |
|
||||
| Width and height must be between 1 and 8000, and one of them is required. | Set a width, a height, or both. | Width |
|
||||
| Quality must be between 1 and 100. | Enter a value from 1 to 100. | Quality |
|
||||
| Fit must be cover, contain or inside. | the "Couldn't save the variant" alert | the form top |
|
||||
|
||||
Rule: Saving an edit
|
||||
|
||||
Scenario: Edit: dirty
|
||||
Given the user changes Quality from 82 to 70 on "hero"
|
||||
Then "Save changes" is enabled, the footer status reads "Unsaved changes", the Quality number reads 70
|
||||
And the output label reads "1200×630 · about 71 KB"
|
||||
And the warning alert stays visible
|
||||
|
||||
Scenario: Save sends PUT with the complete definition
|
||||
When the user activates "Save changes"
|
||||
Then PUT /v1/image-variants/hero is sent with {"width": 1200, "height": 630, "fit": "cover", "format": "webp", "quality": 70, "generate_on_upload": false, "enabled": true} and no "name"
|
||||
|
||||
Scenario: A successful edit reports the purge
|
||||
Given the API answers 200 with quality 70, generated_files 0, cache_size 0 and "purged_files": 244
|
||||
Then the slide-over closes, the row "hero" is updated in place without a refetch (quality 70, Generated 0, Cache size 0 KB)
|
||||
And a toast reads "Variant “hero” saved. 244 files purged."
|
||||
|
||||
Scenario: A switch-only edit says nothing about purging
|
||||
Given the user turned "Generate on upload" on only and the API answers 200 with "purged_files": 0
|
||||
Then the toast reads "Variant “hero” saved."
|
||||
|
||||
Scenario: Saving while saving and the save error behave as in create
|
||||
Then the Saving, 409-free error, other-status and Try again behaviours of the create rule apply with PUT
|
||||
|
||||
Rule: Preview and ImageCompare
|
||||
|
||||
Scenario: Preview layout
|
||||
Then the Preview heading "Preview" is followed, right-aligned and muted, by "Sample: north-wall.jpg"
|
||||
And the compare box is a 3:2 box with a 1 px border and hidden overflow
|
||||
And two labels sit under it: "Original" over "4032 × 3024 · 3.9 MB", and the variant name in mono over "1200×630 · about 96 KB"
|
||||
And the hint "Drag the divider, or focus it and use the arrow keys. The estimate comes from a sample; output is never larger than the source." follows
|
||||
And the images are placeholders, not real photos
|
||||
|
||||
Scenario: Compare box content
|
||||
Given name "hero", Width 1200, Height 630
|
||||
Then the original placeholder fills the box and the variant placeholder covers the part right of the divider (clip-path inset(0 0 0 50%))
|
||||
And a chip "Original" sits 8 px from the top-left and a mono chip "hero" 8 px from the top-right
|
||||
And the divider is a 2 px line at 50 % with a 32 px square handle (44 px on mobile) holding a chevrons-left-right icon centred on it
|
||||
|
||||
Scenario: ImageCompare is a native range input
|
||||
Then the slider is an input type="range" with min 0, max 100, step 1, value 50
|
||||
And it has the accessible name "Compare original and hero" and aria-valuetext "50% hero"
|
||||
And aria-valuenow is 50
|
||||
|
||||
Scenario Outline: Keyboard behaviour
|
||||
Given the slider has focus at value <start>
|
||||
When the user presses <keys>
|
||||
Then the value is <end> and aria-valuetext is "<end>% hero"
|
||||
And the divider and the clip move to <end> %
|
||||
|
||||
Examples:
|
||||
| start | keys | end |
|
||||
| 50 | ArrowRight | 51 |
|
||||
| 50 | ArrowUp | 51 |
|
||||
| 50 | ArrowLeft | 49 |
|
||||
| 50 | ArrowDown | 49 |
|
||||
| 50 | Shift+ArrowRight | 60 |
|
||||
| 50 | Shift+ArrowLeft | 40 |
|
||||
| 50 | Home | 0 |
|
||||
| 50 | End | 100 |
|
||||
| 100 | ArrowRight | 100 |
|
||||
| 0 | ArrowLeft | 0 |
|
||||
| 95 | Shift+ArrowRight | 100 |
|
||||
| 5 | Shift+ArrowLeft | 0 |
|
||||
|
||||
Scenario: Focus ring
|
||||
When the slider has keyboard focus
|
||||
Then the handle shows a 2 px primary outline with a 2 px offset
|
||||
And the slider keeps its value when the form fields change
|
||||
|
||||
Scenario: Pointer dragging
|
||||
When the user drags the handle to the middle of the right half
|
||||
Then the value becomes 75 and the divider follows
|
||||
|
||||
Scenario: No dimension, no comparison
|
||||
Given Width and Height are both empty
|
||||
Then the compare box shows only the original placeholder, with no variant layer, no divider, no handle and no variant chip
|
||||
And the overlay reads "Set a width or height to see a preview." 12 px from the left, right and bottom
|
||||
And the variant label reads "name" over "No output yet" [the board's placeholder name]
|
||||
|
||||
Scenario Outline: Output size, never enlarged
|
||||
Given the sample is 4032 × 3024
|
||||
When the form has Width <w>, Height <h> and Fit <fit>
|
||||
Then the output label dimensions read "<out>"
|
||||
|
||||
Examples:
|
||||
| w | h | fit | out |
|
||||
| 1200 | 630 | cover | 1200×630 |
|
||||
| 800 | | cover | 800×600 |
|
||||
| | 2400 | inside | 3200×2400 |
|
||||
| 600 | 600 | contain | 600×600 |
|
||||
| 1000 | 1000 | inside | 1000×750 |
|
||||
| 8000 | 8000 | cover | 4032×3024 |
|
||||
|
||||
Scenario Outline: Size estimate
|
||||
Given the sample north-wall.jpg
|
||||
When the form has name "<name>", <w>×<h> <fit>, Format <format>, Quality <quality>
|
||||
Then the label reads "<out> · about <estimate>"
|
||||
|
||||
Examples:
|
||||
| name | w | h | fit | format | quality | out | estimate |
|
||||
| hero | 1200 | 630 | cover | WebP | 82 | 1200×630 | 96 KB |
|
||||
| hero | 1200 | 630 | cover | WebP | 70 | 1200×630 | 71 KB |
|
||||
| hero | 1200 | 630 | cover | PNG | n/a | 1200×630 | 310 KB |
|
||||
| thumb | 400 | 300 | cover | WebP | 80 | 400×300 | 18 KB |
|
||||
|
||||
Scenario: The estimate follows quality and format
|
||||
Then a lower quality gives a smaller or equal estimate for the same size and format
|
||||
And the estimate never exceeds the original size 3.9 MB
|
||||
|
||||
Scenario: The sample name and slider labels follow the name
|
||||
Given the name is empty and a width is set
|
||||
Then the chip and label use the placeholder "name" and the slider's accessible name reads "Compare original and name"
|
||||
|
||||
Rule: URL and copy
|
||||
|
||||
Scenario: The URL row
|
||||
Given the name is "hero"
|
||||
Then the label "URL" is followed by a read-only mono 12 px box reading "/static/uploadedImages/north-wall.jpg?v=hero" on --ui-bg-elevated, and a button "Copy" with a copy icon
|
||||
|
||||
Scenario: Without a name
|
||||
Given the name is empty
|
||||
Then the box reads "/static/uploadedImages/north-wall.jpg?v=<name>" in muted text and "Copy" is disabled
|
||||
|
||||
Scenario: Copy writes the URL
|
||||
Given the name is "hero"
|
||||
When the user activates "Copy"
|
||||
Then the clipboard receives the displayed URL
|
||||
And the button reads "Copied" with a check icon
|
||||
And it returns to "Copy" with the copy icon after a short delay (tested with fake timers)
|
||||
|
||||
Scenario: A clipboard failure does not claim success
|
||||
Given navigator.clipboard.writeText rejects
|
||||
Then the button keeps "Copy" and the page raises no error alert
|
||||
|
||||
Scenario: In edit mode the URL uses the locked name
|
||||
Given the edit form of "hero"
|
||||
Then the URL box reads "/static/uploadedImages/north-wall.jpg?v=hero" and "Copy" is enabled
|
||||
|
||||
Rule: Accessibility and hygiene
|
||||
|
||||
Scenario: Status is never colour alone
|
||||
Then every status shows an icon and a word, and dialogs trap focus and return it to the opener
|
||||
|
||||
Scenario: Touch targets on mobile
|
||||
Given the viewport is below 640 px
|
||||
Then buttons, menu items, the switch rows, form controls and the slider handle are at least 44 px high
|
||||
|
||||
Scenario: Vocabulary
|
||||
Then the screens say "variant", "generated" and "purge", never "thumbnail preset", "version" or "rendition"
|
||||
|
||||
Scenario: Hygiene
|
||||
Then no comment is added to any file, and the new components use only Nuxt UI parts and the existing tokens (the only new token allowed is --rc-on-sand, already exported)
|
||||
|
||||
Rule: Estimate module, French and row order
|
||||
|
||||
Scenario: The estimator is one replaceable module
|
||||
Then the size estimate coefficients live in a single named module under the variants area
|
||||
And every place that shows an estimate labels it "about N KB"
|
||||
And the Preview components import the estimate only through that module
|
||||
|
||||
Scenario: French strings
|
||||
Given the locale is French
|
||||
Then every new string of these screens has a French entry in web/src/i18n/messages.ts and no English string appears in the UI
|
||||
|
||||
Scenario: Row order
|
||||
Then tests use the API order (by name) and only the visual-check mock uses the board order
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user