# QA: Schema builder, create flow (SPA slice 3) Runner: Vitest + Vue Test Utils (`task test:web`). No Go test: the POST /v1/content-type contract is already covered by features/custom_content_types.qa.md and nothing changes in the backend. Same pattern as features/schema_builtin_view.qa.md: mock `/v1` through the api composable (extend `views/__tests__/schemaTestKit.ts` with a POST handler and a configurable responder), mount with the real router and a resolved session whose role varies, use `components/__tests__/viewport.ts` for desktop (1440) and mobile (390), fake timers for the 2 s "Copied" and any delay. Never freeze the system date. Tests that touch the clipboard stub `navigator.clipboard.writeText`. Online/offline is driven by dispatching the window `offline` and `online` events. ## Automated Fixtures: the three custom types and five built-in types of schemaTestKit; the "Events" form of the feature (6 fields) as the full-save case; a 201 response for it with id 4 and updated_at; the plain-text bodies `content_type_key_taken`, `invalid_content_type_key`, `invalid_field`. Pure logic (suggested files lib/__tests__/schemaBuilder*.spec.ts; each rule table is an it.each over the feature's Examples): 1. Type key derivation: the 7 name rows; empty result keeps the key empty. 2. Field key derivation: the 7 label rows, including the `_2` suffix against an existing key and the 40-character cut. 3. Type validation: name, key (the 7 rejected rows, the 4 accepted rows including 40 characters), fields count 0 / 1 / 50 / 51, reserved key "new". 4. Field validation: label, key (8 rows), duplicate keys, limits ordering per type, whole non-negative limits for text, default against limits and length, choice rules (blank, duplicate after trim, none, 50 max), unique disabled for richtext and boolean, default dropped when unique. Assert each message string. 5. Problem counting: the 4 rows of the feature (per field and per type-level input, never per message). 6. Payload builder: the 6-field table gives the exact JSON, with null for unset values, [] options, boolean off to null, trimmed name and choices, ISO date bounds, no "icon", no unique default. 7. Sample entry: the 12 value rows, the envelope with fixed ids and timestamps, key fallback "events", field order, skipped invalid keys, 2-space indentation. 8. Type change reset (Number to Yes or no to Choice). Store (extend stores/__tests__/contentTypes.spec.ts): 9. add(type) inserts by name order when status is "ready", does nothing when "idle" (the next load returns it), does not touch built-in items, and does not trigger a request. Composable and API (extend composables/__tests__/useContentTypes.spec.ts): 10. createContentType posts JSON to /v1/content-type and returns the type; ApiError carries status and plain-text body for 400, 409, 422, 403, 500; NetworkError passes through. Builder view (new views/__tests__/SchemaBuilderView.spec.ts and component specs for FieldList, FieldInspector, ChoiceList, ApiPreview), one test per feature scenario: 11. Reaching the builder from the list button, the empty-state button and the mobile floating button; /schema/events and /schema/unknown still show "Content type not found". 12. Roles: owner, admin, a role that is neither (403 page, no field shown), unverified and signed-out redirects (extend routerGuard.spec). Route registered before /schema/:key with the right meta, and GET /v1/builtin-type never called on /schema/new (assert the calls list). 13. State 1 desktop: breadcrumb, Name and Key copy, "1 of 50", starter row, inspector copy, Save and Discard disabled, no chip, collapsed API bar, no "entries use" bar. 14. State 1 mobile: topbar arrow-left and title, stacked Name and Key, "API preview" row, bottom bar, no inspector. 15. Auto key and manual key: pencil, Auto badge, reset, key stays after a name edit. 16. Type-level errors (it.each of the 7 rows) and state 6 together: the three messages, "No fields yet", "3 problems to fix", Save disabled then enabled after fixes, Discard enabled. 17. When errors appear: none on a pristine form; per input while typing; all on Save; aria-describedby and aria-invalid wiring; alert icon on a row in error. 18. Chip table (7 rows). 19. Field list: row content, badges, selected row buttons, "Untitled field" and "–". 20. Add field: appended, selected, label focused, counter, disabled at 50. 21. Inspector per type (it.each over 6 types): labels, unique state with its message, default control, help texts, tiles with icons and descriptions; type change reset. 22. Field rules (it.each over the Examples), unique and default interaction, key rules (state 7) and duplicate key. 23. Choice list: add by button and Enter with focus kept, remove, reorder, default cleared on remove or rename, blank and duplicate flags, 50 cap, counter. 24. Removing a field: selection moves, no undo row, empty list state. 25. Move buttons and disabled ends (it.each); keyboard reorder: Space, Up, Down, Space and Esc, live region text at each step, order restored on Esc. Assert no drag handler on rows. 26. Mobile sheets: row opens the field sheet with Done, focus to the title and back; API sheet with Copy JSON. 27. API preview: routes, texts, sample JSON live update, key fallback, collapsed bar, Copy JSON writes the exact JSON and shows "Copied" for 2 s (fake timers). 28. Save: the exact POST body, one request on double activation, the Saving state (dimmed, disabled controls, loader). 29. Success: toast copy including the singular, store.add, sidebar item, router.replace('/schema'), no leave prompt, the list shows the new row without a second GET /v1/content-type. 30. Key taken: 409 message, key editable with focus and no Auto badge, "1 problem to fix", Save disabled, enabled again after an edit. 31. Server refusals: 400 invalid_content_type_key, 422 invalid_field banner, 403 page, 401 redirect. 32. Save failed: 500 and network, banner copy, edits kept, "Try again" resends the same body. 33. Offline: banner copy, chip, Save disabled, edits kept, recovery without any request. 34. Discard resets to the blank form and sends nothing. 35. Leave guard: pristine, edited, after save; Keep editing, dismiss and Discard changes. Existing tests to update: 36. features/schema_builtin_view.qa.md item 11 and SchemaTypeView.spec: remove /schema/new from the "Unknown key" it.each (it is now the builder). 37. French: every new string has a French entry (the existing i18n completeness spec must pass). ## Visual (task visual:check -- schema) Run `task visual:check -- schema`; every capture within 10 % at desktop and mobile, light and dark. Nothing in the covered states is clock-sensitive. Mock GET /v1/auth/me (owner "Marta Alves") and GET /v1/content-type (the 3 custom types) with the sidebar mock of the existing Schema states, plus every other /v1 call the page makes. The shell and sidebar are the board's `AppFrame`; if the board marks the sidebar differently from the SPA, report it, do not fix it silently. Setups to add in `web/visual/states.ts` (screen `schema`): | Board state | Setup | Sample data (from the frame) | |---|---|---| | New type | open /schema/new, no steps | empty name and key (placeholders "For example, Events" and "events"), starter field Title/title, Text, optional | | Type-level errors | open /schema/new; steps: edit the key and type "e", Remove the starter field, activate Save | empty name, key "e", no fields, chip "3 problems to fix" | | Key already taken | open /schema/new; mock POST /v1/content-type as 409 with the body `content_type_key_taken`; steps: type the name "Events", turn Required on for Title, Add field with the label "Starts on" and the Date tile (key starts_on), select the first row, activate Save | name "Events", key "events", fields Title (required) and Starts on (date), the key error, chip "1 problem to fix" | Known visual differences to report, never hide: the missing `...` button (D15), the absent chip in the pristine state (Q2), and the Key help copy (Q1) if the board text is kept. `skip: 'pending'` (drawn on the Events edit screen, which exists in slice 4): Editing an existing type, Locked key: tooltip, Unsaved changes, Field errors, Choice list editor, Keyboard reorder, API preview open, Saving, Saved, Save failed, Schema incompatible, Offline. Field key error (state 7) has no board frame: Vitest only. If check.ts demands an entry for it, add a pending skip, and report it. The "Review and delete" group is slice 4/5: every state stays pending. ## Manual - Keyboard: Tab order is Name, Key (pencil), Save, Discard, field list (one stop, arrows move between rows), inspector top to bottom, API preview. Screen reader (NVDA or VoiceOver): the live region reads each reorder step; the tile group announces its selected tile; errors are read with their input. - Focus ring on every control; hover and motion on rows, tiles, sheets and the Copy JSON button; sheet open and close focus return on a real phone. - Clipboard: Copy JSON pastes the exact sample entry in a real browser (permission prompt behaviour). - Real backend: as owner, then admin, create a type with all six field types including a Choice default and a unique Title. Check the type in `GET /v1/content-type`, in the sidebar Content group, and in the Schema list. Create the same key again for a real 409. Send an invalid body through curl to compare the 400 and 422 texts with the mapped messages. - Real offline: switch the browser offline, edit, reconnect, press Save. - Reload or close the tab with unsaved edits (not handled by the leave guard): note the browser behaviour.