18 KiB
QA: Media field type in the SPA (slice B)
Runner: Vitest + Vue Test Utils (task test:web). No Go test: the backend is covered by features/media_field_type.qa.md and nothing changes in it. Kit: extend views/__tests__/schemaTestKit.ts with the Events type (7 fields, plus Thumbnail as the 8th, optional or required, saved or new), entries whose data.thumbnail and top-level media vary, the library list (GET /v1/media/?page=&size=&q= with items, total, counts), and the plain-text bodies invalid_entry: thumbnail: required, invalid_entry: thumbnail: type, invalid_entry: thumbnail: unknown_field, schema_incompatible, invalid_field. Library fixtures: 1 "north-wall.jpg", 7 "lime-plaster-workshop.jpg" (1600 × 900, 245760 bytes, shown "240 KB"), 8 "bathroom-tiles.png" (800 × 600, 51200 bytes, "50 KB"), a vector image with null width and height. Pin the time zone; never freeze the system date. No state of this slice depends on the clock.
Automated
Registry and pure logic (lib/tests, it.each over the feature's Examples):
- Registry
media: label, description, icon, control, no pairing, empty value, initial on create and edit, no unique, limits, default or choices, counter none, help, cell, API preview sample null. A loaded field of typemedianever resolves to thetextfallback in the builder, the form or the list. The six existing types still pass every existing registry assertion unchanged (update the count assertions from six to seven). - Validation table: required and optional with null, undefined, 7, 0, -1, 7.5, "7", true, 4294967296. A required field whose image is missing is
required(registry takes the image summary, or the form passes it). - Messages for required and type with the label "Thumbnail"; min, max, options and unique are never produced.
- Payload: id to id, null to null, an optional missing stored id is sent unchanged, never absent. The builder payload of a media field equals
{key,label,type:'media',required,unique:false,default:null,min:null,max:null,options:[]}; a loaded media field round-trips with no diff. - Builder model:
changeFieldTypeto and from media resets unique, default, limits and options and keeps label, key and required. Sample preview value is null. - Review diff: the add row of a media field, required with total > 0 is flagged "Can't be applied" with the exact text; optional reads "Media, optional. N entries are unaffected." (plural rule for 1); total 0 gives no review; making a saved media field required stays "Now required." with "Entries may not fit". Summary line is unchanged.
- Entry helpers:
entryTitlewith a media first field gives "Entry N";describeCellandplainCellTextgive "–" for none and never the id; media summary parsing for existing, missing and absent keys;formatDimensionsandformatSizegive "1600 × 900" and "240 KB" for 245760 bytes and "Vector" for null dimensions.
Schema builder (views/tests/SchemaBuilderMedia*.spec.ts): 8. Seven tiles in order with icon, name and description; 3 columns in the desktop inspector, 2 in the sheet. 9. New media field: "New" badge, key editable with its help, seven tiles enabled, chip "Unsaved changes". 10. Saved media field: key locked with its help, legend lock and "Can't change after saving", six tiles dimmed and unselectable, hint block copy, Required switch and copy, and that "Unique values", "Default value", limit inputs, "Limits" heading and choice list are absent from the DOM. 11. Required marker on the row; row anatomy (28 px image square, label, key, badges). 12. Mobile sheet: title "Field: Thumbnail", footer "Done", one column. 13. Save paths: PUT and POST bodies; 422 invalid_field and 409 schema_incompatible keep their existing flows.
Review (views/tests/SchemaBuilderMediaReview.spec.ts):
14. Refusal: dialog copy, summary, row, badge, note "Back to editing keeps your other changes.", Apply aria-disabled and a click sends no PUT; Back to editing, Esc and close keep the edits and send nothing; turning Required off then Save shows the clean row with Apply enabled.
15. The table of the feature (entries 12 and 0, optional and required, changed to required, mixed with a Capacity change); Apply stays disabled offline in both reviews.
16. A stale count: PUT 409 schema_incompatible runs the existing refusal flow.
MediaFieldControl (components/tests/MediaFieldControl.spec.ts), one test per state and viewport (components/tests/viewport.ts for 1440 and 390):
17. Empty optional and required: structure, copy, " *" hidden from AT, help line, 44 px button on mobile.
18. Filled: thumbnail 160 × 120 (full width 16:9 on mobile), filename, "1600 × 900 · 240 KB", buttons and icons, help line; vector image reads "Vector".
19. Hover bar on desktop only (pointerenter on the thumbnail), absent on mobile; focus ring classes on the keyboard-focused thumbnail.
20. Error: border classes, error line replaces help, aria-invalid and aria-describedby order (error id, help id), focus on "Pick an image" or "Choose another image".
21. Missing optional and required: placeholder role img and name, copy, no help line under the box, buttons.
22. Saving and read-only: form 70 %, thumbnail 60 %, buttons disabled, picker cannot open.
23. Events: emits update:modelValue with the id or null for pick, "Remove image" choice, Remove and Clear; announcements in one polite live region; focus targets after Use this image (Change image), Remove, Clear and "Remove image" (Pick an image), and after Cancel and Esc (the opener).
24. Names and role: group labelled by the label, the exact accessible names, aria-required on the main button, two tab stops as drawn.
Entry editor (views/tests/EntryEditorMedia*.spec.ts):
25. Position after Description, full width, never pairs; form built from the type with Thumbnail.
26. The table "What the control shows and sends" (it.each): loaded value and media map to shown state and PUT body.
27. After a pick the control shows the picked library item's data with no request; after the PUT it is rebuilt from the answered media; after "Reload form" from the refetched entry.
28. Dirty tracking: pick 8 then 7 is clean; pick 8 is dirty and the leave guard prompts.
29. Client validation: required empty and required missing give no request, "Not saved" chip, focus and scroll on the first error in schema order; picking clears the error; optional missing saves with the id unchanged.
30. Server answers: 422 required and type map to the control with their messages and focus; 422 unknown_field on a media key shows the "The Thumbnail field was changed..." line, other keys keep the state 12 copy of features/content_entry_editor.feature; create payload thumbnail: null and thumbnail: 8.
31. Title of an entry whose first field is media: "Entry N" in title, breadcrumb, toast and list card.
Picker field mode (components/tests/ImageUploaderField.spec.ts, views/tests/EntryEditorPicker.spec.ts):
32. Title, tabs, search, "No image" row on optional only and never on required, empty, error and loading states; "Current" badge replaces "In this article"; tile names with and without ", Current"; aria-pressed on tiles and on "No image"; no alt field, no variant select, and no /v1/image-variants request.
33. Selection: strip with 56 px thumbnail, filename and "dimensions · size"; Load more hidden while selected; "Use this image" aria-disabled until a choice; "Remove image" when No image is chosen; selecting a tile releases No image and the reverse; selecting the Current tile then Use closes with no change and no announcement.
34. Library states: empty (field copy with "or entry", and the article copy unchanged), 500 and network failure with Try again repeating only GET /v1/media/, 429 countdown alert, 401 global, loading skeleton after 150 ms (8 and 9 tiles), search trimmed and debounced, no results, Load more focus move.
35. Upload tab: unchanged flow including the duplicate card; a finished upload becomes the field value (per decision D5) and announces "Image selected: ..."; the four upload failure copies.
36. Dialog 640 px wide on desktop, bottom sheet on mobile; Cancel, Esc and close return focus to the opener.
37. Regression: the article picker and the Sites, Authors and SEO pickers keep their copy and behavior (run their existing specs unchanged; if one needs an edit, stop and report).
Entry list (components/tests/FieldCell.spec.ts, EntryTable, EntryCardList, ColumnPicker):
38. Thumbnail, "–" and missing cells: size 40 px, 4 px margins, row height 48 px, role img and name on the placeholder, tooltip on desktop only, alt of the thumbnail (proposed: the filename).
39. Header "Thumbnail": no sort icon, no aria-sort, 80 px; mobile card with a 32 px leading thumbnail and the 68 px minimum height, unchanged when no media column is chosen.
40. Columns menu: Thumbnail listed with the image icon and its key; "4 of 6" and the cap behaviour; stored choice in localStorage.
41. No extra request for thumbnails; a first media column still links to the editor.
Cross-cutting:
42. French: every new string has an entry (the i18n completeness spec passes, including the files added to the list in i18n/contentMessages.spec.ts if any).
43. Existing specs that assert six types or six tiles are updated to seven and nothing else in them changes.
Visual (task visual:check)
Prerequisites in web/visual, no change under docs/design:
check.ts: states of the board groups 5 and 6 that the design markdown does not list (Thumbnail column,Columns menu with Thumbnail,Missing image: tooltip; the tenMedia control: ...states) must be added to the screenscontentandentry. TodayextraSourcesentries whose group equals the screen's primary group are dropped (decision D3), so the runner must be changed to merge them. Groups 2 and 3 (screenschema) and group "10 Media picker for fields" (add toextraSources.entry, board RenewCMS Screens Schema.dc.html) are plainextraSourcesadditions. Group 4 "Media" is a reference gallery with no SPA route: no setup, covered by Vitest. Group 1 "Custom type: read-only view" and group 11 are slice C.states.ts: Events gets an eighth field Thumbnail (media) inschemaEventsFieldsvariants for these states only, so existing states keep their 7 fields. Image URLs (thumbnail_url, libraryurl) are fulfilled by the setup with an SVG stand-in drawing the board's placeholder (repeating-linear-gradient 135deg between --ui-bg-elevated and --ui-bg-accented, mono label "1600 × 900"), asstubBoardImagedoes for the picker. The media size is 245760 bytes soformatSizeprints "240 KB". Sidebar calls stay mocked. Every capture within 10 %, desktop and mobile, light and dark.
| Board group, state | Setup in web/visual/states.ts | Board sample data |
|---|---|---|
| 2, Media field selected | /schema/events, Events + saved optional Thumbnail, 37 entries; step: open the row "Thumbnail" | row "Thumbnail", key "thumbnail", "Field: Thumbnail", "Used in the API, so it can't change.", "Can't change after saving", tiles Text to Media, hint "Holds one image from the media library. ..." |
| 2, New media field, seven types | /schema/events (7 saved fields); step: Add field, label "Thumbnail", click tile Media | "New" badge, key "thumbnail" help "The name used in the API. ...", seven tiles, chip "Unsaved changes" |
| 2, Media field, required | /schema/events, saved Thumbnail required; step: open its row | marker "R" on the row, "Required" switch on with "An entry can't be saved with this field empty." |
| 3, Review: required media field refused | 12 entries (total: 12); step: add Thumbnail, Media, Required on, Save |
"Review changes to Events", "Add 1 field", "Events has 12 entries. ...", "Add Thumbnail", "Can't be applied", "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.", "Back to editing keeps your other changes.", Apply disabled |
| 3, Review: optional media field | same with Required off | "Add Thumbnail", "Media, optional. 12 entries are unaffected.", Apply enabled |
| 5, Thumbnail column | /content/events; localStorage renewcms-content-columns-events = ["title","thumbnail","starts_on","category"]; the 7 standard rows 42, 41, 40, 39, 38, 37, 36 with thumbnail ok, ok, none, missing, ok, none, ok |
header "Thumbnail", 40 px thumbnails, "–" on 40 and 37, dashed unlink square on 39; mobile 32 px leading thumbnail |
| 5, Columns menu with Thumbnail | same storage; step: click "Columns" | "Columns", "4 of 6", Thumbnail with the image icon, "Choose up to 6 fields to show. 4 selected." (mobile) |
| 5, Missing image: tooltip | step: hover the dashed square of row 39 (desktop); mobile has no tooltip and shows the list | "Image no longer in the library" |
| 6, Media control: empty, optional | /content/events/42 with thumbnail null; Events with Thumbnail | "Thumbnail", "No image selected.", "Pick an image", "One image from your library." |
| 6, Media control: empty, required | Thumbnail required, thumbnail null | "Thumbnail *", same body |
| 6, Media control: filled | thumbnail 7, media {id 7, "lime-plaster-workshop.jpg", 1600, 900, 245760} | "lime-plaster-workshop.jpg", "1600 × 900 · 240 KB", "Change image", "Remove", help |
| 6, Media control: hover | filled; step: hover the thumbnail (desktop); mobile is the filled frame | bar "Change image" |
| 6, Media control: focus | filled; step: Tab to the thumbnail (keyboard, so :focus-visible) | 2 px primary ring, name "Change image, lime-plaster-workshop.jpg" |
| 6, Media control: validation error | Thumbnail required, thumbnail null; step: click Save | "Pick an image for Thumbnail.", chip "Not saved", focus on "Pick an image" |
| 6, Media control: image no longer in the library | optional, media {id 7, missing true} | "This image is no longer in the library", "You can still save. The entry keeps the link until you change it.", "Choose another image", "Clear" |
| 6, Media control: missing image, required | required, missing; step: click Save | "Thumbnail is required. Choose another image to save.", "Pick an image for Thumbnail.", "Not saved", focus on "Choose another image" |
| 6, Media control: saving | filled; step: edit Title, Save, PUT hang: true |
"Saving…", thumbnail at 60 % |
| 6, Media control: read-only, type changed | filled; step: edit Capacity, Save, PUT 422 invalid_entry: thumbnail: unknown_field |
"The fields of Events changed", "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.", "Reload form" |
| 10, Library: nothing chosen | /content/events/42 (control holds image 1 "north-wall.jpg", optional); step: click "Change image"; library = the board's 12 images, counts.all 48 | "Choose an image", "Upload", "Library", "Search by filename", "No image", "Leave this field empty.", "Current" on north-wall.jpg, "Newest first · 48 images", "Load more", "Cancel", "Use this image" (disabled) |
| 10, Library: image chosen | step: select the 4th tile (the frame draws id 4 logo-renew.svg, "Vector · 8 KB"; the copy list says kitchen-before.webp, report the mismatch) | strip with filename, "dimensions · size", "Use this image" enabled |
| 10, Library: No image chosen | step: click the row "No image" | sand row with check, "Remove image" |
| 10, Library: required field | Thumbnail required | no "No image" row, "Use this image" disabled |
| 10, Library: empty | GET /v1/media/ returns no item |
"Your library is empty", "Images you upload appear here, ready to use in any article or entry.", "Upload an image" |
| 10, Library: failed to load | GET /v1/media/ 500 |
"Couldn't load your library", "Check your connection and try again.", "Try again" |
| 10, Library: loading | GET /v1/media/ hang: true; wait for the skeleton tile |
skeleton tiles, "Use this image" disabled |
| 10, Upload tab | step: click the tab "Upload" | "Drop an image here, or choose a file", "PNG, JPEG, WEBP, SVG · max 10 MB" |
Board caveats to report, never hide:
- The group 6 media states draw the form with a negative margin (-380 px desktop, -560 px mobile) under an intact title row inside an overflow-hidden box. A real page can only scroll as a whole, so the title row and topbar will differ. If a capture exceeds 10 %, an exception needs the user's explicit approval (
approvedBy), as for existing schema states. Do not callschemaSettleafter scrolling to the control, since it resets scrollTop. - Group 10 draws a stand-in entry form behind the dialog (design conflict 13), not the real editor. Expect a possible exception request for the page behind the dialog.
- The frame and the copy list disagree on "Library: image chosen" (see above) and on the list row titles of "Thumbnail column"; the frame wins.
- Group 6 "Saved" is not checked here; the existing entry setups are untouched.
Manual
- Real backend, owner and a second verified user: create a type with a required Title and an optional Thumbnail; add an entry with an image; the thumbnail shows in the editor and in the list; change the image; remove it; save.
- Delete the image from the Media page, reload the entry: the missing state, then save with the optional field kept (the id is sent unchanged); make the field required through a type with no entries and see "Pick an image for Thumbnail." on save.
- Add a required Thumbnail to a type with entries: the review refuses with Apply disabled; make it optional and apply (real PUT 200). Make an existing optional media field required while an entry has none: real 409, then the existing refusal flow.
- Upload from the Upload tab of the picker with crop and resize, and an already-in-library duplicate; confirm the final selection behaves as decided in D5.
- Keyboard: tab order across the control (thumbnail, Change image, Remove), Enter and Space on a tile, Esc and Cancel return focus to the opener, focus after Use this image, Remove and Clear; screen reader announcements of the live region and of the error; contrast of the Current badge and the hover bar in light and dark.
- Mobile: bottom sheet, 44 px targets, no hover.