92 lines
11 KiB
Markdown
92 lines
11 KiB
Markdown
# QA: Editor language navigation, title, header actions and permission states
|
|
|
|
Accepted together with features/localization_editor_navigation.feature. SPA only, nothing outside web/ changes.
|
|
|
|
## Automated (Vitest + Vue Test Utils, `task test:web`)
|
|
|
|
Mock `useApi`'s `request` per case (no backend). Mount EditorView in DashboardShell with a resolved verified session (user id 7, username "marta") and a test router at `/article/:id/edit`. Use fake timers for the 150 ms delays. Specs live in `web/src/views/__tests__/` and `web/src/composables/__tests__/`, reusing `editorMetaKit.ts`.
|
|
|
|
Fixture (journal, story 5; add `lang`, `story_id`, `capabilities`, `translations` to the article mock): article 10 en online "Renovation costs"; article 12 fr offline "Couts de renovation", updated_at "2026-09-26T11:02:00Z"; article 13 ar online "Takalif"; article 11 de is never in `translations`. `translations` order en, ar, fr. `GET /v1/site/journal/languages` mocked with en (default), fr, de, ar enabled and es disabled. Capability fixtures: full ["read","edit","publish","delete","create_translation"], ro ["read"], p2 ["read","edit"], p3 ["read","publish"], p6 (full on 12, no delete on a sibling). Error mocks reject with `ApiError` carrying a plain-text body.
|
|
|
|
Language menu
|
|
1. **Menu rows**: open the control "Language: French. Switch version"; assert role menu, rows en, ar, fr in order, no German version row (German only as an Add translation row for create_translation), publication badges, current row marked (loc.l5.menu).
|
|
2. **Two and many languages**: two-language site shows two rows, no filter (loc.l5.two); many-language site scrolls and the filter field narrows by name (loc.l5.many).
|
|
3. **Sibling tags**: Examples on sibling capabilities: `["read"]` gives "Read only" tag (loc.perm.siblingReadOnly); absent entry gives no row, no count, no placeholder (loc.perm.siblingHidden); disabled language is listed and marked.
|
|
4. **Add translation rows**: with create_translation the missing languages (including unreadable existing versions) show "Add translation" rows (loc.l5b.withAdd); without it nothing is offered (loc.l5b.noAdd); read-only menu has none (loc.l5b.roMenu).
|
|
5. **Keyboard**: Enter/ArrowDown opens, arrows move, Enter chooses, Escape closes and refocuses the control.
|
|
6. **Single-language site**: no language control, label "Title", primary reads "Publish".
|
|
|
|
Switching
|
|
7. **Clean switch**: choose English; assert `GET /v1/article/10` before `router.push('/article/10/edit')`, then title "Renovation costs".
|
|
8. **Loading**: GET unresolved past 150 ms shows Loading version and keeps the current version (loc.l5.loading); resolved faster shows nothing.
|
|
9. **Switch failed**: GET 10 rejects with 500 and with a network error; toast "Couldn't open the English version" / "You're still editing French. Try again."; route and edits untouched (loc.l5.navFail).
|
|
10. **Version unavailable**: GET 9999 answers 404; only the state with "This version isn't available" and "Back to Articles" renders (loc.l5.gone).
|
|
11. **Unsaved switch dialog, Examples-driven**: Keep editing, Discard and open English, Save and open English; assert dialog copy and button order (loc.l5.unsaved), requests and route per choice, local draft removed on discard.
|
|
12. **Save and open English fails**: PATCH 500; route unchanged, no GET for article 10, Save failed shown.
|
|
|
|
Title
|
|
13. **Title field states, Examples-driven**: pristine, focus, editing (counter "11 / 200"), unsaved, empty ("Enter a title" as alert, aria-invalid, Save blocked), "ab" ("Use at least 3 characters"), 203 characters ("203 / 200", "Use 200 characters or fewer"); ids loc.l5.focus, titleEdit, titleUnsaved, invalid, titleShort, titleLong.
|
|
14. **Title counts code points**: 200 repetitions of the 4-byte emoji is valid.
|
|
15. **Save sends title with the other edits** in one PATCH, shows "Saving title…" (loc.l5.titleSaving), updates header, breadcrumb and menu row on 200, sends no slug.
|
|
16. **Title save failed**: PATCH 500; text kept, "Couldn't save the title. Your text is kept.", "Try again" resends and refocuses the field (loc.l5.failed). A 400 shows the plain-text message in the invalid state.
|
|
|
|
Header and actions
|
|
17. **Action matrix, Examples-driven** over every row of the feature table: assert the single filled button, the secondary buttons, the hints and the online alert; assert at most one filled button for every combination of state and capabilities (loc.l5.default, dirty, dirtyPublish, cleanOnline, onlineDirty, saving, saved).
|
|
18. **Save failed**: banner "Couldn't save French", primary "Try saving again", nothing published (loc.l5.saveFail).
|
|
19. **Collapse**: with the viewport mocked at 1440, 1024 and 390 assert where Unpublish, Edit slug, Move, Delete and the bottom bar render (loc.l5.ovf for the overflow menu and sheet). Touch targets at 390 have class or computed height of at least 44 px.
|
|
20. **Overflow items follow capabilities**, disabled items carry `aria-disabled` and the reason text; P6 shows "You can't delete this article." for Delete article and Move, with "Delete French version" enabled (loc.perm.createNotAll). Edit slug, Move article and Delete article carry `aria-disabled` and the reason "Not available yet." until wired by a later slice; Delete French version stays enabled.
|
|
|
|
Save and publish
|
|
21. **Save and publish order**: confirm dialog copy; assert PATCH 12 resolves before PATCH 12/publish is called; no request for 10, 11 or 13; toast "French published" (loc.l8.saveAndPublish, loc.l8.done).
|
|
22. **Save fails**: publish never called; toast and banner copy (loc.l8.saveFailPublish).
|
|
23. **Publish fails after save**: Saved state, banner "Saved as a draft. Publishing failed. Try publishing again.", "Retry publish" sends only publish (loc.l5.pubFail).
|
|
24. **Language disabled**: publish 422 "language_disabled" shows the warning, Save still works (loc.lang.publishDisabled).
|
|
25. **Clean publish and unpublish dialogs**, Examples-driven, plus failure toast (loc.l8.publish, unpublish, fail).
|
|
26. **Unpublish with unsaved edits**: only `/unpublish` called, edits kept, still unsaved (loc.l8.unpublishDirty).
|
|
|
|
Permissions
|
|
27. **Read-only, Examples-driven over caps**: ["read"] (loc.perm.readonly), ["read","publish"] (loc.perm.publishNoEdit), ["read","edit"] (loc.perm.editNoPublish): banner copy, `aria-readonly`, toolbar hidden or shown, which action buttons exist, Ctrl/⌘ S does nothing without edit.
|
|
28. **Server 403 wins** over capabilities: Save failed with edits kept.
|
|
|
|
Recovery
|
|
29. **401 mirrors the draft** keyed by user 7 and article 12 with title, body, search fields and the mirror time; router goes to `/login?redirect=/article/12/edit&reason=expired`.
|
|
30. **Restore after login**: older server content, stored draft with time "14:18" shown instead; alert "Restored unsaved changes from 14:18"; unsaved state (loc.l11.restored).
|
|
31. **Isolation**: user 8 sees no alert and no restored content; user 7 on article 10 sees none.
|
|
32. **Cleanup, Examples-driven**: saved 200, discard, explicit logout, GET 404, GET without "edit"; the redirect after a 401 does not clear it.
|
|
33. **Draft grants nothing**: stored draft plus 404 shows Version unavailable and removes the draft.
|
|
|
|
Regression
|
|
34. Re-run `EditorView.spec.ts`, `EditorSeo*.spec.ts` and `useArticleDraft.spec.ts`; update the assertions that this feature supersedes (immutable title tooltip, draft key, 404 copy, "Save draft" label) and nothing else.
|
|
|
|
## Visual (`task visual:check`)
|
|
|
|
Reference: docs/design/screens/localization-editor.md and docs/design/screens/localization-conflicts-moves.md, boards in docs/design/source/. Add each state's setup to `web/visual/localizationStates.ts` first, reusing the board's Journal fixture data from `locale-fixtures.js` (titles, languages, counts, the 14:18 restore time). Every capture must be within 10 %, at the listed sizes, light and dark. No exceptions or threshold changes without the user's approval.
|
|
|
|
The checker fails a state with no setup, so run it state by state during the slice: `task visual:check -- localization-editor <state>`, and `task visual:check -- localization-conflicts-moves loc.l11.session`, `... loc.l11.restored`. The full-screen run of localization-editor only passes once localization_translation_flows adds the deletion states; this slice does not run it whole (see open question 9).
|
|
|
|
In scope, with sizes:
|
|
- L5 (1440, 1024, 390): default, dirty, dirtyPublish, cleanOnline, onlineDirty, saveFail, pubFail.
|
|
- L5 (1440, 390): saving, saved, ovf, menu, two, many, unsaved, loading, navFail, gone, focus, titleEdit, titleUnsaved, invalid, titleShort, titleLong, titleSaving, failed.
|
|
- L5b (1440, 1024, 390): perm.readonly, perm.editNoPublish, perm.publishNoEdit.
|
|
- L5b (1440, 390): l5b.roMenu, perm.siblingReadOnly, perm.siblingHidden, perm.createNotAll, l5b.noAdd, l5b.withAdd.
|
|
- L8 (1440, 390): publish, publishDisabled, saveAndPublish, saveFailPublish, unpublish, unpublishDirty, done, fail.
|
|
- L11 (1440, 390): session, restored.
|
|
|
|
Clock-sensitive: none expected. The board draws fixed times (Saved, 14:18); the setups freeze `now` as in features/editor_core.qa.md. The relative "Saved n min ago" form is not asserted visually. If a toast or SaveStatus depends on the real clock in a setup, list it as `skip: clock-sensitive`.
|
|
|
|
## Manual
|
|
|
|
- Focus order: title, language control, status, Unpublish, Save, primary, More actions; focus returns to the control on menu close, to the field on Try again, to the button after a save.
|
|
- Screen reader announcements: "Editing French version", "Saving title", "Title saved", "French saved", "Saved as a draft. Publishing failed. Try publishing again."
|
|
- Hover and pressed states of the language rows, Read only tag contrast, motion (no slide or fade under reduced motion; the spinner becomes a static icon).
|
|
- Real round trip against `task dev`: switch versions, a real 401 after cookie expiry with draft restore, and logout clearing the draft (Vitest mocks `useApi` and `sessionStorage`).
|
|
- Between 1024 and 1440 the header is specified, not drawn: resize by hand and check nothing overlaps.
|
|
|
|
## Known gaps and scope decisions
|
|
|
|
- loc.l11.session (the "Your session expired" modal) is not implemented: the existing redirect to /login?reason=expired stays. The visual checker records it as a known gap.
|
|
- The Add translation row in the language menu renders from capabilities but does nothing until localization_translation_flows.
|
|
- No if_revision is sent in this slice; the conflict dialog and the "Save over theirs" destructive cue belong to localization_translation_flows.
|
|
- features/editor_core.feature and features/editor_core.qa.md are amended in this slice for the editable title, the user and article scoped draft key, the Version unavailable copy and the "Save draft" label.
|
|
- The full-screen run of task visual:check for localization-editor passes only after localization_translation_flows; this slice checks state by state.
|