Files
RenewCMS/features/image_variants_media_screens.feature

431 lines
26 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 on the Media page: badge, filter, sort, Variants tab, delete dialogs and activity
As an owner or admin of a RenewCMS site
I want the Media page to show which images have generated variants, let me generate, preview and purge them for one image, and warn me what a delete also removes
So that I can keep the cache of variants under control without leaving the library
Background:
Given the spec of record is docs/design/screens/image-variants.md sections 3, 4 and 8 (the Generate all and Cache purged: by image rows), docs/design/screens/media.md, docs/design/screens/activity.md, the board docs/design/source/RenewCMS Screens Image variants.dc.html drawn by VariantsFrame.dc.html (frames 3a to 3f and 4a to 4h), and docs/design/routes.md
And this slice is SPA only: the API is the one of features/image_variants_per_image_api.feature and features/image-variants.feature, and features/media_browse.feature and features/media_upload_selection_duplicates.feature stay as they are except where this file says so
And the editor picker is in features/image_variants_editor_picker.feature, and the Settings → Media → Cache screen is not in this slice
And every new English string has a French entry in web/src/i18n/messages.ts, only design copy is verbatim, and French wording is proposed
And the page uses Nuxt UI components only and design tokens, with no raw hex value and no arbitrary Tailwind colour, and no comment is added to any file
And the stored url of an image is "/static/uploadedImages/<storedName>" and the url of its variant is that plus "?v=<name>"
And sizes use the library's formatter ("8 KB", "260 KB", "3.9 MB"), dimensions in the library read "4032 × 3024" and in variants "400×300", "800 × auto", "auto × 2400"
And the library fixture is the 12-image fixture of features/media_browse.feature, extended with variant_count and variant_size from the board:
| id | filename | size | variant_count | variant_size | total |
| 1 | north-wall.jpg | 4089446 | 4 | 266240 | 4.2 MB |
| 2 | kitchen-after.webp | 1258291 | 5 | 419840 | 1.6 MB |
| 3 | kitchen-before.webp | 1153434 | 5 | 404480 | 1.5 MB |
| 4 | logo-renew.svg | 8192 | 0 | 0 | 8 KB |
| 5 | floorboards-copy.jpg | 2516582 | 3 | 245760 | 2.6 MB |
| 6 | IMG_2208.jpg | 2516582 | 0 | 0 | 2.4 MB |
| 7 | IMG_4471.jpg | 3250586 | 2 | 153600 | 3.3 MB |
| 8 | window-frame.jpg | 1887436 | 0 | 0 | 1.8 MB |
| 9 | stone-wall-detail.jpg | 3250586 | 1 | 61440 | 3.2 MB |
| 10 | bathroom-tiles.png | 880640 | 3 | 194560 | 1.1 MB |
| 11 | lime-plaster.jpg | 2202009 | 4 | 286720 | 2.4 MB |
| 12 | floorboards.jpg | 2516582 | 5 | 440320 | 2.8 MB |
And the five enabled variants of the Variants tab, in the order the API returns them (name), are card (inside 800 x auto webp), hero (cover 1200 x 630 webp), og (cover 1200 x 630 jpeg), square (cover 600 x 600 jpeg), thumb (cover 400 x 300 webp), and the disabled variant "print" is never listed
And the Variants tab of image 1 reads its rows from GET /v1/media/1/variants: output 800×600, 1200×630, 1200×630, 600×600, 400×300 and sizes in bytes thumb 18432, card 65536, hero 98304, square 83968, og 145408
And the session role comes from GET /v1/auth/me and is "owner" unless a scenario says otherwise
And every success toast lasts 5 s and toast ids follow the action-plus-id convention
Rule: The library badge
Scenario: A card with generated variants carries the badge
Given GET /v1/media/ resolves 200 with the extended fixture
When the user navigates to /media
Then the card "north-wall.jpg" shows the badge "4 variants" after "Used in 2"
And the card "kitchen-after.webp" shows "Used in 3" then "5 variants"
And the badge has the info-soft fill, the layers icon, 12 px medium text and is 20 px high
Scenario Outline: The badge text and its absence
Given the Default state has loaded
Then the card for "<filename>" shows the badges "<badges>"
Examples:
| filename | badges |
| north-wall.jpg | Used in 2, 4 variants |
| stone-wall-detail.jpg | Duplicate, Used in 1, 1 variant |
| window-frame.jpg | Unused |
| logo-renew.svg | Used in 5 |
| IMG_2208.jpg | Duplicate, Used in 2 |
Scenario: The accessible name of a card is unchanged
Then the card for "north-wall.jpg" is still named "north-wall.jpg, 3.9 MB, used in 2 articles"
Scenario: List view shows no variants badge
Given the Default state has loaded with ?view=list
Then the table and the card list show no "variants" badge
Rule: The variants filter
Scenario: The segmented control sits between the search field and the sort menu
Given the Default state has loaded at desktop width
Then the toolbar reads, left to right, the tabs, the search field, a segmented control "All", "With variants", "Without" (30 px high, 1 px accented border, 12 px horizontal padding) and the sort menu
And "All" is selected, with the primary fill and inverted text
Scenario: On mobile the control is its own full-width row
Given the Default state has loaded below md width
Then the toolbar stacks search, tabs, the segmented control with three equal segments of 44 px and 12 px text, then the sort menu and the grid toggle
Scenario Outline: Choosing a segment writes the URL and requests the list
Given the Default state has loaded
When the user selects "<segment>"
Then the URL <url> and GET /v1/media/ is sent with "<request>" and limit=100
And the list shows a skeleton after 150 ms
Examples:
| segment | url | request |
| With variants | gains ?variants=with | filter=all&sort=newest&variants=with |
| Without | gains ?variants=without| filter=all&sort=newest&variants=without |
Scenario: All removes the parameter
Given the URL is /media?variants=with
When the user selects "All"
Then ?variants leaves the URL and the request carries no variants parameter
Scenario Outline: The query string is the source of truth
When the user opens /media<url>
Then GET /v1/media/ is sent with "<request>" and the control shows "<control>"
Examples:
| url | request | control |
| ?variants=with | filter=all&sort=newest&variants=with | segment "With variants" selected |
| ?variants=without&filter=unused | filter=unused&sort=newest&variants=without | tab "Unused" and segment "Without" selected |
| ?variants=all | filter=all&sort=newest | segment "All" selected, URL kept clean |
| ?variants=zzz | filter=all&sort=newest | segment "All" selected, URL kept clean |
Scenario: With variants returns only the images that have cached files
Given GET /v1/media/?...&variants=with&sort=total_size resolves 200 with ids 1, 7, 9, 12, 5, 11, 2, 3, 10
Then nine cards show in that order and each carries a variants badge
Scenario: Without variants returns the images with no cached file
Given GET /v1/media/?...&variants=without resolves 200 with ids 4, 6, 8
Then three cards show and none carries a variants badge
Rule: Sorting by total size
Scenario: The new option is last in the sort menu
When the user opens the sort menu
Then the options read "Newest", "Oldest", "Name", "Largest", "Largest total size (original + variants)" in that order, the last one 340 px wide on desktop and labelled "Largest total size" on mobile
And the selected option carries a check
Scenario: Choosing it sends sort=total_size
When the user picks "Largest total size (original + variants)"
Then the URL gains sort=total_size and GET /v1/media/ is sent with sort=total_size
And the sort button reads "Largest total size (original + variants)" on desktop and "Largest total size" on mobile
Scenario Outline: The meta line shows the total while sorted by total size
Given the list is sorted by total size
Then the card for "<filename>" shows the meta line "<meta>" in place of "size · dimensions"
Examples:
| filename | meta |
| north-wall.jpg | 4.2 MB total · 3.9 MB + 260 KB |
| kitchen-after.webp | 1.6 MB total · 1.2 MB + 410 KB |
| window-frame.jpg | 1.8 MB total |
| logo-renew.svg | 8 KB total |
Scenario: Other sorts keep the usual meta line
Given the list is sorted by Newest
Then the card for "north-wall.jpg" shows "3.9 MB · 4032 × 3024"
Scenario: A stale sort value is dropped
When the user opens /media?sort=total
Then the sort shows "Newest" and the URL is clean
Rule: Nothing to show with the variants filter
Scenario: With variants and an empty cache
Given GET /v1/media/?...&variants=with resolves 200 with no item and no q is set and the tab is All
Then the layers icon shows with the title "No images with variants", the text "Variants are generated when an image is requested with ?v=<name>, or from its Variants tab." and an outline button "Show all images"
And the toolbar, including the segmented control, stays visible
Scenario: Show all images resets the variants filter
When the user activates "Show all images"
Then ?variants leaves the URL, the segmented control reads "All" and the Default cards reappear
Rule: Delete dialogs name the generated variants they also remove
Scenario: A single delete of an image with variants
Given the detail panel is open for "north-wall.jpg" (used in 2, 4 variants of 266240 bytes)
When the user activates "Delete"
Then the dialog keeps its title "Delete an image that's in use?", its description and its list of the two articles
And a note shows in the body with the layers icon on a warning-soft background: "Also removes 4 generated variants (260 KB)"
And no extra request is sent to compute it
Scenario Outline: The note is singular for one variant and absent for none
Given the detail panel is open for "<filename>"
When the user activates "Delete"
Then the note reads "<note>"
Examples:
| filename | note |
| stone-wall-detail.jpg | Also removes 1 generated variant (60 KB) |
| window-frame.jpg | no note is shown |
Scenario: Bulk delete sums the selection
Given the user selected "north-wall.jpg", "kitchen-after.webp" and "window-frame.jpg"
When the user activates "Delete" in the bulk bar
Then the bulk dialog keeps its copy and its one row per image
And the note under the rows reads "Also removes 9 generated variants (670 KB)"
Scenario: A bulk delete with no variant in the selection shows no note
Given the user selected "window-frame.jpg" and "IMG_2208.jpg"
Then the dialog shows no note
Scenario: The delete flow itself is unchanged
Then the pending, failure, 404 and success behaviours of features/media_browse.feature apply, and after a successful delete the list is requested again in the background
Rule: The Variants tab in the detail panel
Scenario: The panel gets two tabs for an owner or an admin
When the user opens the panel of "north-wall.jpg"
Then two tabs sit under the 56 px header: "Details" (selected) and "Variants", 40 px high on desktop and 44 px on mobile, 20 px apart, with a 2 px primary underline on the selected one
And the Details tab is the existing panel body, unchanged
Scenario: A session that is not owner or admin sees no tab
Given the session role is neither "owner" nor "admin"
When the user opens a panel
Then no tab strip shows, no request to /v1/media/{id}/variants is sent, and the panel is the existing one
Scenario: Selecting the tab requests the variants once
When the user selects "Variants"
Then GET /v1/media/1/variants is sent
And selecting "Details" then "Variants" again does not send it a second time for the same open panel
Scenario: Loading
Given GET /v1/media/1/variants has not answered
Then nothing is drawn in the tab body for 149 ms
And at 150 ms a region announces "Loading variants" with aria-busy="true" and holds 5 skeleton rows
Scenario Outline: The variants request fails
Given GET /v1/media/1/variants fails with <failure>
Then an error alert shows the title "Couldn't load variants", the text "Check your connection and try again." and a "Try again" button with a rotate-cw icon
When the user activates "Try again"
Then the request is sent again
Examples:
| failure |
| status 500 |
| a network failure |
Scenario: Partly generated
Given GET /v1/media/1/variants resolves with card, hero, square and thumb generated and og not
Then the tab shows, in order: the Original block, the summary, the list
And the Original block (72 x 54 thumbnail, 12 px gap, 12 px padding, 1 px border, --ui-bg-muted) reads "Original · kept untouched", "north-wall.jpg", "JPEG · 4032 × 3024 · 3.9 MB"
And the summary reads "4 of 5 generated" in semibold next to a "Generate all" button with the zap icon
And the list is a bordered group of five rows in the order card, hero, og, square, thumb
Scenario Outline: A generated row
Then the row "<name>" shows its name in mono 13 px medium, the badge "Generated" (success-soft, circle-check), and the meta "<meta>"
And the part "<saving> vs original" is in the success colour and the meta before it is toned 12 px text
Examples:
| name | meta | saving |
| thumb | 400×300 · WebP · 18 KB · -99% | -99% |
| card | 800×600 · WebP · 64 KB · -98% | -98% |
| hero | 1200×630 · WebP · 96 KB · -98% | -98% |
| square | 600×600 · JPEG · 82 KB · -98% | -98% |
Scenario: A not generated row
Then the row "og" shows the badge "Not generated" (outline, circle-dashed) and the meta "1200×630 · JPEG" with no size and no saving
Scenario Outline: The saving against the original
Given the original is 4089446 bytes
When a variant file is <size> bytes
Then the saving reads "<text>"
Examples:
| size | text |
| 18432 | -99% |
| 65536 | -98% |
| 98304 | -98% |
| 145408 | -96% |
| 3250586 | -21% |
| 4089446 | 0% |
| 4500000 | +10% |
Scenario: Nothing generated yet
Given GET /v1/media/1/variants resolves with no row generated
Then the summary reads "0 of 5 generated" over the muted line "Nothing generated yet."
And every row shows "Not generated"
Scenario: Row actions on desktop are icon buttons
Given the viewport is 640 px wide or more
Then a generated row shows three 28 px square icon buttons Copy URL (link), Preview (eye) and Purge (eraser), each with a tooltip and an accessible name equal to its label
And a not generated row shows Copy URL (link) and Generate now (zap)
Scenario: Row actions on mobile are labelled buttons on their own line
Given the viewport is below 640 px
Then each action is a 44 px high labelled button on a line under the meta, "Copy URL", "Preview", "Purge" or "Copy URL", "Generate now"
Scenario: Generate all is disabled when everything is generated
Given all five rows are generated
Then "Generate all" is disabled
Scenario: Generate now
When the user activates "Generate now" on the row "og"
Then POST /v1/media/1/variants/og is sent
And the row "og" shows the badge "Generating" (info-soft, loader-circle), its actions are gone, its background is --ui-bg-elevated and the other rows stay as they are
And the summary shows "Generating variants…" in the info colour and "Generate all" reads "Generating…" with a loader-circle and is disabled
When the API answers 200 with the item generated
Then the row shows "Generated" with its meta, the summary reads "5 of 5 generated"
And a toast shows icon circle-check, id "media-variant-generated-1-og" and title "Generated og"
Scenario: Generate all
Given only thumb is generated
When the user activates "Generate all"
Then POST /v1/media/1/variants is sent
And the four rows not generated show "Generating", the generated row stays, and the summary and button read as above
When the API answers 200 with "generated" 4 and the five items
Then every row shows "Generated" and a toast shows id "media-variants-generated-1" and title "Generated 4 variants"
Scenario: A single generated variant reads in the singular
When Generate all answers "generated" 1
Then the toast title is "Generated 1 variant"
Scenario: Generate all with a failure
When Generate all answers 200 with "generated" 4, "failed" 1, failures [{"name": "hero", "cause": "too_large"}] and the five items
Then the row "hero" shows the badge "Failed" (error-soft, circle-x), a line with a circle-alert icon in the error colour at 12 px reading "Image too large", and a "Retry" button with a rotate-cw icon (labelled, 28 px high, 12 px text)
And the summary reads "4 of 5 generated" and the toast reads "Generated 4 variants"
Scenario Outline: A single generation fails with a cause
When POST /v1/media/1/variants/hero answers <status> with the plain text body
Then the row "hero" shows "Failed" and the line "<cause>" with "Retry"
And an error toast shows the title "Couldn't generate hero." and the text "<cause>."
Examples:
| status | cause |
| 413 | Image too large |
| 415 | Unsupported source format |
Scenario: Failed state is not remembered
Given the row "hero" shows "Failed"
When the panel is closed and opened again, and the tab is selected
Then the row "hero" shows "Not generated"
Scenario: Retry sends the same request
When the user activates "Retry" on the row "hero"
Then POST /v1/media/1/variants/hero is sent and the row shows "Generating"
Scenario Outline: Other failures of a generation
When POST /v1/media/1/variants/og answers <status>
Then the row "og" returns to "Not generated"
And <result>
Examples:
| status | result |
| 500 | a toast shows "Something went wrong" and "The server couldn't finish. Try again. If it keeps happening, check the server log." |
| 403 | a toast shows "You don't have access" and "Only admins can manage image variants." |
| 404 | a toast shows "Not found" and "This variant or image no longer exists. Refresh the page." and the tab is requested again |
| network failure | the same toast as 500 |
Scenario: The image is gone
When the tab is requested again after a 404 and GET /v1/media/1/variants answers 404
Then the panel closes, the image leaves the list and the info toast "That image no longer exists" shows, as in features/media_browse.feature
Scenario: Copy URL copies the absolute variant url
When the user activates "Copy URL" on the row "thumb"
Then "{origin}/static/uploadedImages/<storedName>?v=thumb" is written to the clipboard, also for a row that is not generated
And a toast shows id "media-variant-url-copied-1-thumb", title "URL copied" and text "Paste it wherever you need the image address."
Scenario: Purge a generated row
When the user activates "Purge" on the row "thumb"
Then DELETE /v1/media/1/variants/thumb is sent
When the API answers 200 {"purged_files": 1, "purged_bytes": 18432}
Then the row shows "Not generated" with no size, the summary reads "3 of 5 generated" and a toast shows id "media-variants-purged-1-thumb" and title "Purged 1 file (18 KB)"
Scenario Outline: Purge fails
When DELETE /v1/media/1/variants/thumb answers <status>
Then the row stays "Generated" and the toast of the back-office error table for <status> shows
Examples:
| status |
| 403 |
| 404 |
| 500 |
Scenario: Preview
When the user activates "Preview" on the row "thumb"
Then a dialog opens, 880 px wide on desktop and a bottom sheet on mobile, titled "Preview: thumb", with a "Close" button
And two panes sit side by side on desktop (stacked on mobile), each 300 px high (180 px on mobile), bordered, on --ui-bg-muted, overflow scrolling to pan
And the left pane shows the variant file at actual size under "thumb · 400×300 · WebP" and "18 KB · saves 99%"
And the right pane shows the original at actual size under "Original · 4032×3024 · JPEG" and "3.9 MB"
And the hint reads "Both images are shown at actual size. Scroll to pan."
And focus is trapped in the dialog and returns to the Preview button on close
Scenario: No variants defined
Given GET /v1/media/1/variants resolves with no item and "unsupported" null
Then the Original block shows, then the empty block with the layers icon, the title "No variants defined", the text "Define a variant in Settings to get resized copies of this image." and a link "Settings → Media → Variants" with an arrow-up-right icon to /settings/media/variants
And no summary, no list and no "Generate all" show
Scenario: An SVG source
Given the panel is open for "logo-renew.svg" and GET /v1/media/4/variants resolves {"unsupported": "svg", "items": []}
Then the Original block reads "SVG · Vector · 8 KB"
And an info alert shows the title "Variants aren't generated for this format." and the text "SVG images are served as they are."
And no summary, no list and no "Generate all" show
Rule: Generate all and purge by image in the Activity screen
Scenario Outline: The two new sentences
Given an activity event with action "<action>" and outcome "success" and target name "north-wall.jpg"
Then the Action cell reads "<sentence>" with "north-wall.jpg" in semibold, last
Examples:
| action | sentence |
| media.variant_generate_all| Generated all variants of north-wall.jpg |
| media.variant_purge_image | Purged the variants of north-wall.jpg |
Scenario Outline: The failed sentences
Given an activity event with action "<action>", outcome "failed" and target name "north-wall.jpg"
Then the Action cell reads "<sentence>" and the Failed badge shows
Examples:
| action | sentence |
| media.variant_generate_all| Failed to generate the variants of north-wall.jpg |
| media.variant_purge_image | Failed to purge the variants of north-wall.jpg |
Scenario Outline: The detail line under the sentence
Given an activity event with action "<action>" and params <params>
Then a muted 12 px line under the sentence reads "<detail>" in the row and in the details panel
Examples:
| action | params | detail |
| media.variant_generate_all| {"generated": 5, "failed": 0, "causes": []} | 5 variants generated |
| media.variant_generate_all| {"generated": 1, "failed": 0, "causes": []} | 1 variant generated |
| media.variant_generate_all| {"generated": 4, "failed": 1, "causes": ["too_large"]} | 4 generated · 1 failed (Image too large) |
| media.variant_generate_all| {"generated": 3, "failed": 2, "causes": ["too_large", "unsupported_format"]} | 3 generated · 2 failed (Image too large, Unsupported source format) |
| media.variant_purge_image | {"purged_files": 4, "purged_bytes": 266240} | Removed 4 files (260 KB) |
| media.variant_purge_image | {"purged_files": 1, "purged_bytes": 18432} | Removed 1 file (18 KB) |
Scenario: Events without a detail line are unchanged
Given an event whose action has no detail line, such as "media.rename"
Then the row shows no second line
Scenario: The French interface
Given the locale is French
Then the two sentences, their failed forms and the detail lines come from messages.ts and no English string appears
Rule: Accessibility, vocabulary and hygiene
Scenario: Status is never colour alone
Then each row status shows an icon and a word (Generated circle-check, Not generated circle-dashed, Generating loader, Failed circle-x) and each icon-only button has an accessible name and a tooltip
Scenario: Touch targets on mobile
Given the viewport is below 640 px
Then the segmented control, the tabs, the row buttons and the dialog buttons are at least 44 px high
Scenario: Progress is announced
When a generation starts and ends
Then an aria-live="polite" region announces "Generating variants" and the result
Scenario: Vocabulary
Then the screens say "variant", "generated" and "purge", never "thumbnail preset", "version" or "rendition"
Scenario: French strings
Given the locale is French
Then every new string of this file has a French entry and no English string appears