# QA procedure: relation field in the SPA Two parts. Part 1 is automated (Vitest component tests, `task test:web`). Part 2 is the visual gate (`task visual:check`). Part 3 is manual. The Go side is in `relation_field_type.qa.md`. Until `task test:web` exists, SPA tests are reported NOT RUN, never passing. ## Part 1: automated (Vitest + Vue Test Utils) Mock `/v1` per test with the shapes below; no live server. A field is `{"key","label","type":"relation","required","unique":false,"default":null,"min":null,"max":null,"options":[],"relation":{"content_type","kind"}}` and every other field carries `"relation": null`. Registry 1. `lib/fieldTypes`: the relation entry has the properties of the registry outline (label, icon `lucide:link`, control `RelationField`, no unique/limits/default/choices, empty value per kind, sample null or []). An unknown type still falls back to text; a relation never does. 2. Client validation table (single and many, required/optional, 0, "3", [3], duplicates, 51 ids) and the message map including the "not produced" rules. Builder (one test per state) 3. Type gallery: Relation is the last tile; locked on a saved field with the other tiles dimmed. 4. New relation: the related-type select lists the other types (mock `GET /v1/content-type`), the help text, the three cards with the exact sentences, default "One to many" (proposed); choosing a type updates the sentences. 5. Preview outline: the kind change updates both sentences and the line `· shown on Venue as the field "Events"`; the example cards "Event" and "Venue" show the board's chips (Rue Verte hall, Open day, Lime plaster workshop, +1). Nothing about the inverse is in any request. 6. Save body: relation sent as `{"content_type","kind"}`, others `"relation": null`; a loaded field round-trips unchanged; no related type chosen blocks Save with the proposed message. 7. Saved relation: key, tiles, related type and cards are disabled; Label and Required editable; the proposed lock note shows. 8. Review: required relation + entries > 0 shows the flagged row and disabled "Apply changes" with no PUT; optional shows the clean row; required with 0 entries sends at once; making an optional relation required shows "Entries may not fit" with Apply enabled. 9. Server answers: 409 `schema_incompatible` and 422 `invalid_field` map to the existing flows. 10. Delete refused: 409 `content_type_referenced` shows the proposed copy and the type stays listed. 11. A non-owner/admin sees no builder. Entry form (one test per state) 12. Single control: type "rue" -> only `GET /v1/content-type/venues/entry?q=rue&page=1&size=...` is sent (debounced, trimmed, stale answers ignored); rows show title and second line; pick sets the value to 3, one chip, "Unsaved changes", live message. 13. Many control: chips for [11, 12], "Search to add", the help line; add keeps order; x and Backspace remove; picked ids are not offered again; the 51st is blocked with the proposed message. 14. States: loading (skeleton after 150 ms), no match with "Clear search", empty type, 500, network error, 429 countdown, 401 redirect, 404 (all with the proposed copy where tagged), "Try again" re-sends only the search. 15. Missing target (optional and required), saving, keyboard (arrows, Enter, Esc, Tab, Backspace), required-empty error and focus order (first invalid field only). 16. Outline "What the control shows and sends" (all 7 rows) and the create payload with `venue: null`, `speakers: []` and with a picked number. 17. Dirty tracking: back to the original value gives "Saved"; reordered many ids count as changed. 18. Server answers table: `required`, `type`, `unique`, `unknown_field` (reload alert), each with the right copy, chip and focus. 19. Layout: a relation after boolean/number/date never pairs. List 20. Cell outline (one chip, two chips, two chips + "+2", dash for null, dash for [], missing chip) from each item's `relations` with no extra request; relation columns are not sortable (no sort icon, no `aria-sort`). 21. Desktop grid for items 42/41/40 matches the board texts; the mobile card shows the labelled lines; the Columns menu lists Venue and Speakers with the link icon and the cap of 6. 22. Plain text of a relation cell never shows the numeric id; a first-field relation keeps the link and the "Entry N" title. Accessibility 23. Combobox/listbox roles and attributes, chip group label, "Remove X from Y" button names, required/invalid/describedby wiring, the live region texts. ## Part 2: visual (`task visual:check -- `) Hard gate: each capture within 10 % at 1440x900 and 390x844, light and dark. Add setups in `web/visual/states.ts` with the board's own sample data; no clock-sensitive state exists in this group. | Screen | State | Board sample data the mock must reuse | | --- | --- | --- | | schema | Relation in the builder | Events with Venue (label), key `venue`, related type Venue, "One to many" selected, preview sentences, example cards with chips "Rue Verte hall", "Open day", "Lime plaster workshop", "+1" | | entry (content/events/42) | Relation in the entry form | Title "Lime plaster workshop" (21 / 80); Venue search "rue" with rows "Rue Verte hall / Lyon · 120 seats", "Rue Verte yard / Lyon · outdoor", "Rue du Port studio / Nantes · 24 seats"; Speakers chips "Ines Duarte", "Jonas Pereira"; help "An event can have many speakers." | | content (events) | Relation in the list | Items 42 "Lime plaster workshop" 21 Nov 2026 (Rue Verte hall; Ines Duarte, Jonas Pereira, +2), 41 "Heat pumps: what the numbers say" 3 Nov 2026 (–; Jonas Pereira), 40 "Open day at the Rue Verte site" 12 Nov 2026 (Rue Verte yard; –) | Notes: - The board's states are drawn with a "Later" badge; the SPA does not render it. Expect a pixel difference at the badge. If the 10 % gate fails because of it, the user decides on a documented `exception` (`approvedBy`); the QA never adds one. - The board's builder shows a four-tile gallery plus Relation; the SPA shows the full gallery. The mismatch is reported to the user, not worked around. - States tagged "(proposed copy)" (loading, no match, error, missing chip, locked inspector, review refusal, delete refusal) are not on the board and get Vitest tests only, no visual capture. ## Part 3: manual (screenshots can't cover) - Keyboard and focus order across the combobox, chips and list; screen reader announcements of the live region. - Hover and motion of the popover and of the chips' x; the mobile bottom-sheet versus under-input choice. - Real SQLite end to end with the built binary: build the type pair, pick, save, reload, delete a target and see the missing chip. - `bunx playwright install chromium` the first time on a machine.