# 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: : `, 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 1. 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. 2. 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. 3. Route resolution (it.each): "new", "42", "abc", "0" (resolves to id 0, sent as is: the API answers 404 entry_not_found), "4294967296", "-1". 4. 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. 5. Defaults (it.each from the feature table): value shown, tag shown, tag leaves on change and returns on restore; no tag in edit. 6. 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. 7. Page title (it.each): text, null → "Entry 42", richtext first line, number; unchanged while typing. 8. Controls and help lines (it.each): one control per type, placeholders, Off/On text, empty select "None" item on optional select only. 9. Layout rule (pairing of date, number, boolean; full width for text, richtext, select; single column below md). 10. 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. 11. 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). 12. Payloads: create and edit bodies exactly as in the feature, explicit nulls, no unknown key, boolean table. 13. State 5 Saving: chip, button, dimming, disabled controls, no second request on click or Enter. 14. 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. 15. State 7 Save failed (500, network, 400, 422 with no field): alert copy, chip, values kept, Try again resends the same body. 16. State 8 Required: no request, all messages, chip "Not saved", `document.activeElement` is 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). 17. State 9 Rules: one message per field, message table (it.each), server answers that pass the client map to their field and take focus. 18. Errors clear per field; chip returns to "Unsaved changes"; no blur validation. 19. State 10 Unique (409): alert and field message, per-type messages, alert clears when the field is edited. 20. 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. 21. Content type 404, 403 and 401; load failure variants with "Try again" resending only the failed request; loading skeleton with aria-busy. 22. 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. 23. 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. 24. 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). 25. Stale answers dropped; navigating between routes carries nothing. 26. 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_id` and `sort` is checked against the Go handler and corrected with the list code and tests if it differs. 27. Accessibility: labels, aria-required, live-region chip, tablist keyboard, toolbar names, dialog focus. 28. 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/2026` as 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.