Files
RenewCMS/features/media_field_spa.feature
T

493 lines
35 KiB
Gherkin
Raw 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: Media field type in the SPA
As an owner or admin building a content type, and as a verified user filling an entry
I want a Media field that holds one image from the media library
So that entries link to a library image and always show its current version
Background:
Given the SPA calls the API with relative URLs under /v1 and the "jwt" HttpOnly cookie
And API errors are plain text
And this slice changes only web/ (and web/visual); the backend of features/media_field_type.feature is done
And the design is docs/design/media-field-changes.md sections 1, 2, 3.2 to 3.7, 4, 6 and 7, with the board groups 2 Schema builder, 3 Review and delete, 4 Field types, 5 Content and entry list, 6 Entry editor and 10 Media picker for fields of "RenewCMS Screens Schema.dc.html"
And the registry is lib/fieldTypes with lib/entryEditors, the single source for the builder, the list cells and the entry form; no screen branches on the type name "media" where a registry property can say it
And a media value is the id of a library image (a uint32 of at least 1) or null
And every entry answer (POST 201, PUT 200, GET one, each list item) carries a top-level "media" object keyed by media field key: {"id","filename","width","height","size","thumbnail_url"} for an existing image, or {"id","missing":true} when the image is gone; a null value has no key
And the API refuses with 422 "invalid_entry: <key>: required" or "invalid_entry: <key>: type", 409 "schema_incompatible" for a new required media field on a type with entries, and 422 "invalid_field" for forbidden options
And the picker is the existing ImageUploader with MediaPicker, loading GET /v1/media/?page=&size=&q= with the total in the "counts" and "total" fields as the article picker does; the Sites, Authors, SEO and article pickers are unchanged
And the picker in field mode never requests /v1/image-variants and never shows the variant select or the alt text
And the form dimming stays the entry form's 70 % (design conflict 12, existing rule kept); inside the control the thumbnail drops to 60 % and the buttons take the disabled look
And sentence case, "image" and never "asset", buttons are verbs
And every string goes through t() and has a French entry in i18n/messages.ts
And a copy tagged "(proposed copy)" is not in the design and is the coder's single point of change
And user decisions already taken: Apply is disabled for the certain refusal; thumbnail after Description in schema order; "Clear"; "1600 × 900 · 240 KB"; "Use this image" and "Remove image"; "Pick an image" on optional fields; row heights 48 and 68
And out of scope (slice C): "Used in" badge, usage panel and delete warning of the Media library, the read-only view of a custom type, filtering by media
Scenario Outline: Media in the registry
Given the registry entry for "media"
Then <property> is <value>
Examples:
| property | value |
| type name in the API | media (the seventh entry, after select) |
| label | Media |
| description | One image from your media library |
| icon | lucide:image |
| control | MediaFieldControl |
| pairs with a neighbour | no, full width (as text, richtext and select) |
| empty value | null |
| initial on create | null; a media field has no default, so no "Default" tag ever shows |
| initial on edit | the stored id, or null |
| supports unique | no |
| supports limits | no |
| supports default | no |
| supports choices | no |
| counter | none |
| help | One image from your library. |
| list cell | thumbnail, column 80 px, not right-aligned, not sortable |
| sample in the API preview | null |
Scenario Outline: Client validation of a media value
Given a media field "Thumbnail" that is <required>
When the value is <value>
Then the rule is <rule>
Examples:
| required | value | rule |
| required | null | required |
| required | undefined | required |
| optional | null | none |
| optional | 7 | none |
| required | 7 | none |
| optional | 0 | type |
| optional | -1 | type |
| optional | 7.5 | type |
| optional | "7" | type |
| optional | true | type |
| optional | 4294967296 | type |
Scenario Outline: Messages of a media field
Given a media field labelled "Thumbnail"
Then the message of the rule <rule> reads <message>
Examples:
| rule | message |
| required | Pick an image for Thumbnail. |
| type | Thumbnail must be an image from the library. |
| min | not produced for media |
| max | not produced for media |
| options | not produced for media |
| unique | not produced for media |
Scenario: A loaded media field is not shown as Text
Given GET /v1/content-type/events returns a field {"key":"thumbnail","label":"Thumbnail","type":"media","required":false,"unique":false,"default":null,"min":null,"max":null,"options":[]}
Then the builder, the entry form and the list resolve it to the media entry of the registry, never to the fallback type "text"
# ---------- Schema builder (groups 2 and 4)
Scenario: Seven type tiles
Given the inspector of a field that can still change type
Then seven radio tiles are shown in this order, each with its icon, name and description
| tile | icon | description |
| Text | type | Short text, like a title or a name |
| Rich text | pilcrow | Long text with formatting, written in Markdown |
| Number | hash | Whole or decimal numbers |
| Yes or no | toggle-left | An on or off switch |
| Date | calendar | A calendar day, without a time |
| Choice | list | One option from a list you define |
| Media | image | One image from your media library |
And the grid has 3 columns in the desktop inspector and 2 in the mobile sheet
Scenario: Adding a media field (design "New media field, seven types")
Given Events with 7 saved fields and 37 entries
When the user activates "Add field", types the label "Thumbnail" and chooses the tile "Media"
Then the new row shows the badge "New", the key "thumbnail" (Auto, editable) with the help "The name used in the API. Starts with a letter. Lowercase letters, digits and underscores."
And the seven tiles are all enabled with Media selected and the legend "Type" has no lock
And the chip reads "Unsaved changes"
And the row is: grip, a 28 px square holding the image icon, the label, the key in mono and the badge "New"
Scenario: The inspector of a media field shows only what applies (design "Media field selected")
Given Events with a saved optional media field "Thumbnail" as its eighth field
When the user selects the row "Thumbnail"
Then the inspector reads "Field: Thumbnail" and holds, in this order, the Label input, the locked Key "thumbnail" with "Used in the API, so it can't change.", the Type tiles, the switch "Required" with "An entry can't be saved with this field empty." and the hint block
And the legend of the tiles reads "Type" with a lock and "Can't change after saving", Media is selected and the six other tiles are dimmed to 40 % and not selectable
And the hint block is info-soft with an info icon and reads "Holds one image from the media library. Entries link to the image instead of keeping a copy, so they always show its current version."
And "Unique values", "Default value", the limit inputs, the "Limits" heading and the choice list are not rendered at all
Scenario: Changing a new row to and from Media
Given a new row of type Text, required, with a Longest of 80
When the user chooses the tile "Media"
Then the label, the key and Required are kept and unique, default, limits and choices are reset
When the user then chooses the tile "Number"
Then the media hint is gone and the number controls show with no stale value
Scenario: A required media field (design "Media field, required")
Given a saved media field "Thumbnail" that is required
Then its row carries the existing Required marker "R" (accessible name "Required") next to the key
And its inspector shows Required switched on
Scenario: The PUT and POST body of a media field
Given a media field with the label "Thumbnail", the key "thumbnail" and Required off
When the type is saved
Then the field is sent as {"key":"thumbnail","label":"Thumbnail","type":"media","required":false,"unique":false,"default":null,"min":null,"max":null,"options":[]}
And a round trip of the loaded field to the payload produces no change
Scenario: Media in the API preview
Given the builder holds a media field "Thumbnail"
Then the sample entry shows "thumbnail": null and no "Example text"
Scenario: The mobile sheet of a media field
Given a 390 px viewport
When the user selects the row "Thumbnail"
Then the 92 % sheet titled "Field: Thumbnail" shows the same inspector in one column with the type tiles in 2 columns and the footer button "Done"
Scenario: The field-type gallery (group 4) has no SPA screen
Then the registry facts above are covered by the registry scenarios and no route renders the gallery frame
# ---------- Review (group 3)
Scenario: A required media field added to a type with entries is refused first (design "Review: required media field refused")
Given Events with 12 entries and a new media field "Thumbnail" that is required
When the user activates Save
Then no PUT is sent
And the dialog "Review changes to Events" with "Apply them to save the content type." opens (a bottom sheet on mobile)
And the summary reads "Add 1 field" and "Events has 12 entries. Each one is checked against the new fields when you apply."
And the row "Add Thumbnail" has the warning badge "Can't be applied" and the text "Media, required. A media field can't have a default, so the 12 existing entries would have no image. Make Thumbnail optional to add it."
And the note "Back to editing keeps your other changes." shows instead of the "Flagged changes can be refused..." note
And "Back to editing" is enabled and "Apply changes" is disabled (aria-disabled), so pressing it sends nothing
Scenario: The refusal is only for a new required media field with entries
Given Events with <entries> entries and the edit <edit>
When the user activates Save
Then <result>
Examples:
| entries | edit | result |
| 12 | adds Thumbnail, Media, optional | the review opens, "Apply changes" is enabled |
| 12 | adds Thumbnail, Media, required | the review opens, "Apply changes" is disabled |
| 0 | adds Thumbnail, Media, required | no review, the PUT is sent at once |
| 12 | makes the saved optional media field Thumbnail required | the review opens, row "Change Thumbnail" "Now required." flagged "Entries may not fit", Apply enabled; the server decides |
| 12 | adds Thumbnail required and changes Capacity | both rows listed, Apply disabled |
Scenario Outline: The clean row (design "Review: optional media field")
Given Events with <entries> entries and a new optional media field "Thumbnail"
Then the row "Add Thumbnail" reads "<text>" with no flag
And "Apply changes" is enabled
Examples:
| entries | text |
| 12 | Media, optional. 12 entries are unaffected. |
| 1 | Media, optional. 1 entry is unaffected. (proposed copy) |
Scenario: Back to editing after the refusal
Given the refused review is open
When the user chooses "Back to editing" (or presses Esc or the close icon)
Then the dialog closes, every edit is kept and no request was sent
When the user turns Required off on the new row and activates Save
Then the review opens with the clean row and "Apply changes" enabled
Scenario: The refusal follows offline and applying
Given the refused review is open
Then "Apply changes" stays disabled whatever the connection
And with a clean review, "Apply changes" is still disabled while offline, as before
Scenario: The server refuses anyway
Given the entry count was stale and the client opened no refusal
When the PUT answers 409 "schema_incompatible"
Then the existing "Nothing was changed" refusal flow of features/schema_builder_edit.feature runs unchanged
Scenario: 422 invalid_field on a media field
Given the PUT answers 422 "invalid_field"
Then the existing invalid-field failure of the builder is shown and the edits stay
# ---------- MediaFieldControl in the entry editor (group 6)
Scenario: Where the control sits
Given Events with the fields Title, Starts on, Ends on, Online, Capacity, Category, Description and Thumbnail
Then the control for Thumbnail is the eighth, after Description, full width in the 760 px column, and the footer lines come after it
Scenario: Empty, optional (design "Media control: empty, optional")
Given /content/events/new, or an entry whose thumbnail is null
Then the label reads "Thumbnail"
And a dashed box (1 px, border-accented, padding 12) holds a 40 px square with the image icon, the text "No image selected." and the outline button "Pick an image" with the image-plus icon
And the help line reads "One image from your library."
And on mobile the box stacks and the button is 44 px high
Scenario: Empty, required (design "Media control: empty, required")
Given the field is required
Then the label reads "Thumbnail *" with the asterisk hidden from assistive technology, and the rest is as the empty state
Scenario: Filled (design "Media control: filled")
Given an entry whose thumbnail is 7 and media.thumbnail is {"id":7,"filename":"lime-plaster-workshop.jpg","width":1600,"height":900,"size":245760,"thumbnail_url":"/media/lime-plaster-workshop.jpg"}
Then a solid 1 px box (border-accented, padding 12) holds a 160 × 120 thumbnail button showing thumbnail_url, the filename in weight 500, "1600 × 900 · 240 KB" in 12/18 muted, the outline button "Change image" (image icon) and the ghost button "Remove" (x icon)
And the help line reads "One image from your library."
And on mobile the thumbnail is full width at 16:9 and the two buttons are full width and 44 px high
And a width or height of null (a vector image) reads "Vector · 8 KB"
Scenario: Hover (design "Media control: hover")
Given the filled state on desktop
When the pointer is over the thumbnail
Then the box border takes the --ui-text-muted colour and a 28 px bar at the bottom of the thumbnail (--ui-bg-inverted, --ui-text-inverted) reads "Change image" with the image icon at 14 px
And mobile has no hover state and shows the filled state
Scenario: Focus (design "Media control: focus")
Given the filled state
When the keyboard focus reaches the thumbnail button
Then it has a 2 px --ui-primary outline with a 2 px offset
Scenario: Pointer and keyboard open the picker
Given the filled state
When the user activates the thumbnail, "Change image", or on an empty control "Pick an image"
Then the picker opens in field mode and nothing else changes
Scenario: Remove
Given the filled state
When the user activates "Remove"
Then the value becomes null, the control shows the empty state and the chip reads "Unsaved changes"
And the live region says "Image removed from Thumbnail." and the focus is on "Pick an image"
Scenario: Missing image, optional (design "Media control: image no longer in the library")
Given media.thumbnail is {"id":7,"missing":true} and the field is optional
Then the box is solid with a dashed 160 × 120 placeholder (16:9 on mobile) holding image-off at 24 px, with role "img" and the name "Image no longer in the library"
And beside it "This image is no longer in the library" in weight 500, the help "You can still save. The entry keeps the link until you change it." in 12/18 muted, the outline button "Choose another image" and the ghost button "Clear"
And there is no help line under the box
And "Save" is allowed and the data sent keeps thumbnail 7
Scenario: Clear
Given the missing state
When the user activates "Clear"
Then the value becomes null, the live region says "Thumbnail cleared." and the focus is on "Pick an image"
Scenario: Missing image, required (design "Media control: missing image, required")
Given media.thumbnail is {"id":7,"missing":true} and the field is required
Then the in-box help reads "Thumbnail is required. Choose another image to save."
And when the user saves, no request is sent, the box takes the error border, the line "Pick an image for Thumbnail." shows with a circle-alert icon, the chip reads "Not saved" and the focus is on "Choose another image"
Scenario: Validation error on an empty required field (design "Media control: validation error")
Given an empty required media field
When the user activates "Save"
Then no request is sent
And the box border is --ui-error with an inset 1 px --ui-error, the error line "Pick an image for Thumbnail." with circle-alert at 14 px replaces the help line, and the chip reads "Not saved"
And the focus moves to "Pick an image" and the page scrolls to it
And only the first field in schema order with an error takes the focus
Scenario: Picking clears the error
Given the validation error is shown
When the user picks an image
Then the error and the error border go, the chip reads "Unsaved changes" and nothing else is validated
Scenario: Saving (design "Media control: saving")
Given the PUT has not answered
Then the form is dimmed to 70 %, the thumbnail is at 60 %, the buttons take the disabled look, the thumbnail button and the buttons are not operable and the picker cannot open
And Save reads "Saving…"
Scenario: Read-only after a schema change (design "Media control: read-only, type changed")
Given the PUT answers 422 "invalid_entry: thumbnail: unknown_field" and thumbnail is a media field of the loaded type
Then a warning alert "The fields of Events changed" reads "The Thumbnail field was changed after you opened this form. Reload the form to continue. What you typed here can't be saved against the old fields." with the action "Reload form"
And the control looks as in Saving and cannot be operated
And "Reload form" fetches the type and the entry again and rebuilds the form with the control from the answered media map
And any other key keeps the copy of features/content_entry_editor.feature state 12
Scenario Outline: What the control shows and sends
Given the loaded value <loaded> with media <media>
When the user <action> and activates "Save"
Then the control shows <shown>
And the data holds "thumbnail": <sent>
Examples:
| loaded | media | action | shown | sent |
| null | none | saves with nothing changed | the empty state | null |
| 7 | existing image 7 | saves with nothing changed | the filled state | 7 |
| 7 | missing image 7 (optional) | saves with nothing changed | the missing state | 7 |
| 7 | existing image 7 | picks image 8 | image 8 (its own library data) | 8 |
| 7 | existing image 7 | activates Remove | the empty state | null |
| 7 | missing image 7 | activates Clear | the empty state | null |
| null | none | picks image 8 | image 8 | 8 |
Scenario: Right after a pick the control uses the picked library item
Given the user picks "bathroom-tiles.png" (id 8, 800 × 600, 51200 bytes) from the library
Then before any save the control shows its filename, "800 × 600 · 50 KB" and its url as the thumbnail
And no request is made for it
When the PUT answers 200 with media.thumbnail for image 8
Then the control is rebuilt from the answered media map
Scenario: Dirty tracking of a media value
Given an entry whose thumbnail is 7
When the user picks image 8 then picks image 7 again
Then the chip reads "Saved", "Save" is disabled and the leave guard does not prompt
When the user picks image 8
Then the chip reads "Unsaved changes" and the leave guard prompts
Scenario Outline: Server answers for a media field
Given the client found no error and the PUT or POST is sent
When the server answers <status> "<body>"
Then <result>
Examples:
| status | body | result |
| 422 | invalid_entry: thumbnail: required| "Pick an image for Thumbnail." under the control, error border, chip "Not saved", focus on the first button |
| 422 | invalid_entry: thumbnail: type | "Thumbnail must be an image from the library." under the control, chip "Not saved", focus on "Change image" or "Pick an image" (the image was deleted between the pick and the save) |
| 409 | schema_incompatible | not an entry answer; never produced by the entry routes |
Scenario: Create payload with a media field
Given /content/events/new with Title "Open day", Starts on 2026-11-21 and the media field left empty
When the user activates "Save"
Then POST /v1/content-type/events/entry is sent once with "thumbnail": null among the other keys, never absent
And with image 8 picked it holds "thumbnail": 8, a JSON number
Scenario: The title of an entry whose first field is media
Given a type whose first field is a media field
Then the page title, the breadcrumb and the saved toast read "Entry 42" (an id is not a title) and the list card title reads the same (proposed)
Scenario: Layout of the generated form with a media field
Given a boolean, a number or a date field before the media field
Then the media field never pairs and is full width at every viewport
# ---------- Picker in field mode (group 10)
Scenario: Opening the picker from an optional field (design "Library: nothing chosen")
Given the control holds image 1 "north-wall.jpg" and the field is optional
When the user activates "Change image"
Then the dialog (640 px wide on desktop, a bottom sheet on mobile) is titled "Choose an image" and has the tabs "Upload" and "Library", opening on Library
And the search reads "Search by filename"
And the first row of the Library tab is "No image" with "Leave this field empty."
And the summary reads "Newest first · 48 images", the tiles are square, 4 columns on desktop and 3 on mobile, and "Load more" shows
And the tile of image 1 carries the badge "Current" (sand fill, --rc-on-sand text, 18 px high, bottom left)
And "Cancel" is enabled and "Use this image" is disabled (aria-disabled)
And there is no alt text field, no variant select, and no request to /v1/image-variants
Scenario: Choosing an image (design "Library: image chosen")
When the user selects a tile
Then it gets the primary outline with a check at top right, aria-pressed is true, and a strip appears below the grid with a 56 px thumbnail, the filename and "dimensions · size"
And the grid shrinks and "Load more" is hidden while a tile is selected, as in the article picker
And "Use this image" is enabled
When the user activates it
Then the dialog closes, the value becomes that image's id, the control shows its data, the live region says "Image selected: <filename>" and the focus is on "Change image"
Scenario: Choosing "No image" (design "Library: No image chosen")
When the user selects the row "No image"
Then it takes the sand fill, the primary border and a check, with aria-pressed true, and any selected tile is released
And the primary button reads "Remove image"
When the user activates it
Then the value becomes null, the live region says "Image removed from Thumbnail." and the focus is on "Pick an image"
Scenario: A required field has no "No image" row (design "Library: required field")
Given the field is required
Then the Library tab shows no "No image" row and the title is still "Choose an image"
Scenario: Choosing the current image again
Given the control holds image 1
When the user selects the tile "Current" and activates "Use this image"
Then the dialog closes, the value does not change, the chip stays as it was, and no announcement is made (proposed)
Scenario: Empty library (design "Library: empty")
Given GET /v1/media/ returns no image
Then the picker opens on the Upload tab unless the user already chose a tab, and the Library tab reads "Your library is empty" / "Images you upload appear here, ready to use in any article or entry." with "Upload an image"
And "Upload an image" switches to the Upload tab
And the article picker keeps "Images you upload appear here, ready to use in any article."
Scenario Outline: Library failures (design "Library: failed to load")
Given GET /v1/media/ answers <outcome>
Then <result>
Examples:
| outcome | result |
| 500 | the error alert "Couldn't load your library" / "Check your connection and try again." with "Try again", no "No image" row |
| a network error | the same |
| 429 | the countdown alert of the Media page |
| 401 | the global redirect, no local copy |
Scenario: Try again
Given the failed state
When the user activates "Try again"
Then only GET /v1/media/ is sent again and the grid replaces the alert
Scenario: Loading (design "Library: loading")
Given GET /v1/media/ has not answered
Then after 150 ms skeleton tiles show (8 on desktop, 9 on mobile), no "No image" row shows and "Use this image" is disabled
Scenario: Search and no results
When the user types "wall"
Then the search is trimmed and debounced, the selection is released and the grid reloads
And an empty match reads "No images match \"wall\"" with "Clear search", as in the article picker
Scenario: Load more
When the user activates "Load more"
Then the next page loads, the focus stays on the button until the new tiles arrive and then moves to the first new tile
Scenario: Upload tab (design "Upload tab")
When the user opens the Upload tab
Then the existing flow runs unchanged: "Drop an image here, or choose a file", "PNG, JPEG, WEBP, SVG · max 10 MB", crop and resize, the duplicate check "Already in your library" with "Use existing" and "Upload anyway"
And a finished upload lands in the library and becomes the value of the field, as in the Sites and Authors pickers, then the dialog closes and the live region says "Image selected: <filename>" (to confirm, decision D5)
Scenario Outline: Upload failures keep the existing copy
Given the POST /v1/media/ answers <outcome>
Then <copy> shows and nothing is chosen
Examples:
| outcome | copy |
| 413 | That file is too large. |
| 400 | That request wasn't valid. Check the fields and try again. |
| 429 | Too many requests. Try again in a few minutes. |
| 500 | Something went wrong on our side. Nothing was changed. Try again in a moment. |
Scenario: Cancel and Escape
When the user activates "Cancel" or presses Esc or the close icon
Then nothing changes and the focus returns to the button that opened the picker
Scenario: Tile names in field mode
Then each tile is a button named "stone-steps.jpg, 310 KB" and "stone-steps.jpg, 310 KB, Current" for the current image, with aria-pressed, and no "used in" part
And "No image" is a button with aria-pressed and "Use this image" uses aria-disabled while nothing is chosen
Scenario: The other pickers are unchanged
Then the article picker still reads "Add an image", "Insert image", "In this article", the alt text and the variant select
And the Sites, Authors and SEO pickers still read "Use image" with no strip and no "No image" row
# ---------- Entry list (group 5), minimal safe cell
Scenario: The Thumbnail column (design "Thumbnail column")
Given Events with a Thumbnail media field, the columns Title, Thumbnail, Starts on and Category chosen, and the items 42 and 41 with an existing image, 40 and 37 with null, 39 with a missing image
Then the header reads "Thumbnail" with no sort icon and no aria-sort, in a column 80 px wide
And an existing image shows a 40 px square thumbnail from media.thumbnail_url, with 4 px above and below, so the row stays 48 px
And a null value shows "–" in the muted colour
And a missing image shows a dashed 40 px square with the unlink icon, role "img" and the name "Image no longer in the library"
And the thumbnail is read from the item's own "media" object, with no extra request
Scenario: Tooltip of the missing placeholder
Given a missing image in the list on desktop
When the pointer or the focus reaches the dashed square
Then a tooltip reads "Image no longer in the library"
And mobile has no tooltip
Scenario: The mobile card (design "Thumbnail column", mobile)
Given a 390 px viewport and the Thumbnail column chosen
Then a 32 px thumbnail leads each card, "–" stands for none, a dashed 32 px square for a missing image, and the card keeps its 68 px minimum height
And when no media field is among the chosen columns the card is unchanged
Scenario: Columns menu (design "Columns menu with Thumbnail")
When the user opens "Columns"
Then "Thumbnail" is listed with the image icon, its key "thumbnail", and a checkbox like the other fields
And the summary reads "4 of 6" and on mobile "Choose up to 6 fields to show. 4 selected."
And the cap of 6, Reset and the stored choice work as for any field
Scenario: Thumbnail as the first column
Given a type whose first field is a media field
Then the first column still links to the editor and the thumbnail is the link content
Scenario: Plain text of a media cell
Given the card or a title needs the text of a media cell
Then it reads "–" for none and never the numeric id
# ---------- Accessibility (media-field-changes.md section 6)
Scenario: Accessibility of the control
Then the control is role="group" labelled by the field label
And the main button (Pick an image, or Change image) carries aria-required when the field is required
And the accessible names are "Pick an image for Thumbnail", "Change image, lime-plaster-workshop.jpg, 1600 by 900, 240 KB" (the thumbnail), "Remove image from Thumbnail", "Clear Thumbnail" and, for the placeholder, role "img" "Image no longer in the library"
And the error text has an id, and the main button gets aria-invalid="true" and aria-describedby with the error id then the help id
And the tab order is label, thumbnail, Change image, Remove, then help or error (two stops open the same picker, as drawn; decision D8)
And one polite live region announces "Image selected: lime-plaster-workshop.jpg", "Image removed from Thumbnail." and "Thumbnail cleared."; nothing is announced when the picker opens
And focus goes to the first invalid control on a refused save and, after the picker, to the places of the picker scenarios
And mobile targets are 44 px and there is no hover on mobile
Scenario: Accessibility of the builder and the review
Then the Media tile is a radio in the existing radio group, the dimmed tiles are disabled, "Can't be applied" is a text badge and not only a colour, and the disabled "Apply changes" is announced as disabled