Files
RenewCMS/features/image_variants_screens.feature

690 lines
38 KiB
Gherkin
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.
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 bottom sheet on mobile
Given the viewport is below 640 px
When the user activates "Add variant"
Then a tall bottom sheet opens as specified in features/variants_visual_conformance.feature, "Add variant is a tall bottom sheet on mobile", superseding the former full-page layout
And the form is followed by the Preview section, stacked, separated by a top border
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