8.3 KiB
QA: Entry editor of a custom content type (SPA slice 7)
Runner: Vitest + Vue Test Utils (task test:web). No Go test: the five entry endpoints and /v1/user/lookup already exist and the backend does not change. Kit: reuse views/__tests__/schemaTestKit.ts and add GET/POST/PUT/DELETE handlers for /v1/content-type/{key}/entry[/{id}] with the plain-text bodies entry_not_found, content_type_not_found, invalid_entry: <key>: <rule>, and the Events type with its 7 fields and entry 42 of SchemaFrame.dc.html. Pin the time zone. Never freeze the system date: "Saved at" is derived from the mocked updated_at, so the expected text is built with the same local formatter from that value.
Automated
- Registry (lib/fieldTypes spec, it.each over the six types): empty value, default pre-fill, control component, help line, counter rule, validate (required, type, min, max, options), message text, payload value (null for empty, false for a required boolean). The builder and the list cells still pass on the same registry.
- Entry API (composable spec): getEntry, createEntry, saveEntry, deleteEntry; the body is {data}; error parsing of "invalid_entry: key: rule" with status 422 and 409 into {key, rule, status}; 404 bodies; 400, 401, 403 and NetworkError pass through; lookupUsers is sent once with distinct ids.
- Route resolution (it.each): "new", "42", "abc", "0" (resolves to id 0, sent as is: the API answers 404 entry_not_found), "4294967296", "-1".
- State 1 New entry: title, subtitle, chip "New entry", asterisks, Default tag on Online, helps, placeholders, footer "Not saved yet" and "–", Save enabled, Delete absent; mobile layout.
- Defaults (it.each from the feature table): value shown, tag shown, tag leaves on change and returns on restore; no tag in edit.
- State 2 Editing: breadcrumb, title, "Entry 42", chip "Saved", Save disabled, footer names and dates after the lookup; "–" with the date kept when the lookup fails or lacks an id; mobile trash-2 action.
- Page title (it.each): text, null → "Entry 42", richtext first line, number; unchanged while typing.
- Controls and help lines (it.each): one control per type, placeholders, Off/On text, empty select "None" item on optional select only.
- Layout rule (pairing of date, number, boolean; full width for text, richtext, select; single column below md).
- State 3 Unsaved: counter "32 / 80", "91 / 80" in the error colour without blocking, no counter without max, chip, Save enabled, revert returns to clean.
- State 4 Preview: rendering of bold and list, raw HTML shown as text, unsafe links inert, empty preview, tab switch keeps value and caret, toolbar inserts (it.each).
- Payloads: create and edit bodies exactly as in the feature, explicit nulls, no unknown key, boolean table.
- State 5 Saving: chip, button, dimming, disabled controls, no second request on click or Enter.
- State 6 Saved: edit and create; toast; chip "Saved at HH:MM" from the answer; footer update; create replaces the URL to /content/events/43 with no extra GET; no leave prompt; edit after save is dirty again.
- State 7 Save failed (500, network, 400, 422 with no field): alert copy, chip, values kept, Try again resends the same body.
- State 8 Required: no request, all messages, chip "Not saved",
document.activeElementis the first erroring control in schema order for each type (input, textarea, select trigger, date segment), aria-invalid and aria-describedby, per-type required messages (it.each). - State 9 Rules: one message per field, message table (it.each), server answers that pass the client map to their field and take focus.
- Errors clear per field; chip returns to "Unsaved changes"; no blur validation.
- State 10 Unique (409): alert and field message, per-type messages, alert clears when the field is edited.
- State 11 Not found: GET 404, PUT 404, DELETE 404 and bad ids with no request; "Back to Events" goes to /content/events with no prompt.
- Content type 404, 403 and 401; load failure variants with "Try again" resending only the failed request; loading skeleton with aria-busy.
- State 12 Schema changed: unknown key, unknown_field, required on an unknown key, 409 on an unknown key; read-only form; Save disabled; Cancel enabled; chip "Not saved"; "Reload form" refetches the type (and the entry) and rebuilds the form; reload failure.
- State 13 Delete: dialog copy, focus on Cancel, Cancel/Esc/close, no request; Deleting lock; 204 replaces the URL and shows the toast; 404, 403, 401, 500 and network outcomes; delete with unsaved edits.
- Leave guard (it.each from the feature table), including defaults-only new form untouched, and Cancel history rule (previous entry is the list vs not).
- Stale answers dropped; navigating between routes carries nothing.
- Reaching the editor from the list: "New entry" and row links resolve to the editor (update the slice-6 specs that expected the catch-all 404). The slice-6 Background wording about
content_type_idandsortis checked against the Go handler and corrected with the list code and tests if it differs. - Accessibility: labels, aria-required, live-region chip, tablist keyboard, toolbar names, dialog focus.
- French: every new string has an entry (i18n completeness spec passes).
Visual (task visual:check -- entry)
Register the group "6 Entry editor" in extraSources if needed. Every capture within 10 %, desktop and mobile, light and dark. Mock the sidebar's GET /v1/content-type and /v1/user/lookup. No state is clock-sensitive: "Saved at" comes from the response timestamp. If the runner cannot pin the zone, build updated_at as the builder's Saved setup does.
| Board state | Setup in web/visual/states.ts | Board sample data |
|---|---|---|
| 1 New entry | /content/events/new, Events type | "New entry", "Not saved yet", Online Off + "Default" |
| 2 Editing | /content/events/42, lookup 3 and 5 | "Lime plaster workshop", "Entry 42", "Inès Duarte · 12 Oct 2026, 09:14", "Marta Alves · 15 Oct 2026, 11:02" |
| 3 Unsaved changes | step: type ", two days" in Title | "Lime plaster workshop, two days", "32 / 80", focus ring on Title |
| 4 Markdown preview | step: click "Preview" on Description | "Two days of hands-on work with lime putty and old stone." and the two bullets |
| 5 Saving | PUT hang: true after an edit |
"Saving…" |
| 6 Saved | PUT 200 with updated_at 2026-10-19 11:02 local, updated_by 5 | "Saved at 11:02", "Entry saved", "Marta Alves · 19 Oct 2026, 11:02" |
| 7 Save failed | PUT 500 after an edit | "Couldn't save the entry" |
| 8 Required fields empty | /content/events/new, click Save | "Fill in Title.", "Pick a date for Starts on." |
| 9 Rule errors | /content/events/42 with the board's values typed, PUT 422 for any server-side one | "91 / 80", "Capacity must be at most 5000.", "Choose one of: Talk, Workshop or Meetup." |
| 10 Duplicate (409) | /content/events/new, title "Heat pumps: what the numbers say", POST 409 | "Another entry already uses this Title. Enter a different one." |
| 11 Entry not found | /content/events/999, entry 404 entry_not_found | "Entry not found" |
| 12 Schema changed | /content/events/new, POST 422 invalid_entry: room: required | "The fields of Events changed" |
| 13 Delete entry: confirm | /content/events/42, step: click "Delete" | "Delete this entry?" |
Board caveats to report, never hide:
- State 9 draws
31/11/2026as text in Ends on; a segmented date control cannot hold it, so that field will differ. - State 11's breadcrumb shows the entry title, unknown on a 404.
- State 12's chip is drawn "Saved" and its Cancel is drawn disabled; the spec uses "Not saved" and an enabled Cancel (user decision).
- Mobile in state 9 depends on the chosen layout rule.
Manual
- Real backend with two users: create an entry; Back from it returns to the list, not the empty form; edit; delete; the list shows the change.
- Two tabs: change the type's fields in one (add a required field), save the entry in the other: state 12, "Reload form". Delete the entry in one tab and save in the other: state 11.
- Duplicate a unique title for a real 409.
- Keyboard: tab order across fields, toolbar, tabs, footer buttons; Enter in a single-line field saves; focus lands on the first error; dialogs trap focus.
- Markdown with raw HTML and
javascript:links stays inert in Preview. - Reload on /content/events/42 keeps the page; the browser back from a saved new entry goes to the page before the new form.