690 lines
38 KiB
Gherkin
690 lines
38 KiB
Gherkin
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
|