Files

127 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QA: Editor screen — load, edit and save (core)
## Implementation note for coder (informational, not a QA assertion)
`docs/design/components.md`'s `ErrorView` only documents two fixed copies (`kind: 'not-found'` →
"Page not found" / Home, and `kind: 'error'` → the generic retry copy). The editor's own 404-on-
load state needs different copy ("This article doesn't exist." / "All articles" → /article),
per `docs/design/states.md`'s HTTP-status-to-copy table. Either widen `ErrorView` with an optional
title/description/action override, or add a third `kind`; this is a same-layer prop widening, not
a new component, so it doesn't need its own slice.
Known limitation, not a bug to silently fix: per `features/public_article_read.feature`,
`GET /v1/article/{id}` falls back to public online-only visibility on a malformed/expired jwt
(no 401). So if a session is already stale when the owner opens an *offline* article, the GET
answers 404 exactly like a truly deleted article — the editor cannot tell the two apart from this
response alone, and correctly shows the "This article doesn't exist." state.
## Automated (Vitest + Vue Test Utils, `task test:web`)
Mock `useApi`'s `request` for `GET /v1/article/{id}` and `PATCH /v1/article/{id}` per case; no
real backend, no httptest server. Mount EditorPage inside DashboardShell with a resolved verified
session (username "marta") and a test router at `/article/12/edit`. Use fake timers for the
150 ms skeleton delay and the relative-time SaveStatus format. Use `Object.defineProperty`/mocked
`navigator.onLine` and dispatched `window` `online`/`offline` events for the offline scenarios.
Fixture article (reused across cases unless a scenario overrides a field): id 12, title
"How we price a bathroom renovation", is_online false, body length 4318, created_at
"2026-03-12T09:14:00Z", updated_at "2026-09-26T11:02:00Z" (fixed `now`
"2026-09-26T11:02:00Z" unless a scenario advances it).
1. **Skeleton only after 150 ms** — mock `GET /v1/article/12` unresolved; assert no skeleton
before 150 ms; advance past it; assert skeleton placeholders for title/content/details;
resolve 200; assert the Saved state renders. Separately, resolve under 150 ms; assert no
skeleton ever renders.
2. **Saved state, draft and online** — Examples-driven on `is_online`: mock `GET
/v1/article/12` resolving 200 with the fixture at each `is_online` value; assert the
breadcrumb, ArticleStatusBadge ("Draft"/"Online"), SaveStatus "Saved at 11:02", Save
disabled, PublishControl ("Publish"/"Unpublish"), and the neutral count ending "4,318 characters".
3. **Unsaved changes** — from the Saved state, type into the body; assert SaveStatus "Unsaved
changes", Save enabled, PublishControl now "Save and publish".
4. **Saving via click and via Ctrl/⌘ S** — from Unsaved changes, (a) click Save, (b) press
Ctrl+S; in each, assert `PATCH /v1/article/12` is called with the edited body, SaveStatus
"Saving…", Save disabled while unresolved.
5. **Successful save** — from Saving, resolve `PATCH` 200 with the updated article; assert
SaveStatus "Saved at {time}" and Save disabled again.
6. **Relative save time after an hour** — after a 200 save at 11:02, advance the fake clock past
one hour with no further edit; assert SaveStatus switches to "Saved {n} min ago".
7. **Save failed, Examples-driven** — (a) `PATCH` resolves 500, (b) `PATCH` rejects (network);
in each, assert SaveStatus "Save failed" with a visible Retry button, and the editor still
shows the typed body.
8. **Retry recovers** — from Save failed (500), click Retry; assert `PATCH` is called again with
the same body; resolve 200; assert SaveStatus "Saved at {time}".
9. **404 on save shows a toast, not the error SaveStatus** — from Unsaved changes, save; resolve
`PATCH` 404; assert a toast "That article no longer exists." appears and SaveStatus never
reads "Save failed".
10. **Very long body** — set the body's HTML length to 120000 (via a fixture string of that
length); assert the count "19,845 words · 120,000 characters" with no warning styling, Save
and PublishControl enabled.
11. **No limit states** — assert no "Near the limit" or "Too long" text is ever rendered.
12. **Preview toggle** — from a loaded article with body content, switch Preview on; assert the
toolbar and editable surface are gone and a read-only rendering of title+body appears;
switch off; assert the editable surface returns with the same content.
13. **Offline blocks and recovers** — from Unsaved changes, set `navigator.onLine` false and
dispatch `window` `offline`; assert SaveStatus shows the wifi-off icon and reads "Waiting
for connection", Save and PublishControl disabled with a tooltip; set `navigator.onLine`
true and dispatch `online`; assert Save re-enables and SaveStatus reads "Unsaved changes"
again.
14. **Leave guard: Discard changes proceeds** — from Unsaved changes, click the "Articles"
breadcrumb link; assert ConfirmDialog opens with the exact title/text/button labels; click
"Discard changes"; assert the router navigates to `/article` and no further `PATCH` fires.
15. **Leave guard: Keep editing stays** — same setup; click "Keep editing"; assert the dialog
closes and the router does not navigate, and the unsaved body is still shown.
16. **No leave guard when clean** — from the Saved state, click the "Articles" breadcrumb link;
assert the router navigates to `/article` with no ConfirmDialog ever rendered.
17. **Session expiry mid-save mirrors and restores the draft** — from Unsaved changes with body
"Paragraph added just before the session expired.", save; resolve `PATCH` 401; assert
`sessionStorage` holds that body keyed by article id 12, and the router navigates to
`/login?redirect=/article/12/edit&reason=expired`. Then, simulating the return trip, mount
EditorPage again at `/article/12/edit` with `GET /v1/article/12` resolving 200 with a body
that lacks that paragraph; assert the rendered body is the restored sessionStorage draft
(with the paragraph) and SaveStatus reads "Unsaved changes".
18. **Not found on load** — mock `GET /v1/article/9999` resolving 404; mount at
`/article/9999/edit`; assert only the error state renders (title "This article doesn't
exist.", action "All articles"), with no breadcrumb-title/SaveStatus/Save/PublishControl/
ArticleEditor; click "All articles"; assert the router navigates to `/article`.
## Visual (manual verification — desktop and mobile, light and dark)
Reference: `docs/design/screens/editor.md` and `docs/design/source/RenewCMS Screens
Back-office.dc.html`, group "Editor". Run `task visual:check -- editor` for each state below and
compare against the board frame; add each state's mock setup to `web/visual/states.ts` first,
reusing the sample data below and a fixed `now` of "2026-09-26T11:02:00Z" (updated_at "Today,
11:02"; created_at "12 Mar 2026, 09:14"; id 12; title "How we price a bathroom renovation";
3 images).
- [ ] Loading: skeletons for title, content and details, shaped like the real layout.
- [ ] Saved: breadcrumb, "Draft" badge, "Saved at 11:02", disabled Save, "Publish", counter
"N words · 4,318 characters", details panel (Created/Updated/ID/Images/Public API URL).
- [ ] Unsaved changes: "Unsaved changes" SaveStatus, enabled Save, "Save and publish".
- [ ] Saving: "Saving…" SaveStatus, disabled Save.
- [ ] Save failed: "Save failed" with Retry, per the SaveStatus error spec.
- [ ] Very long article: count "19,845 words · 120,000 characters", neutral, Save/Publish enabled.
- [ ] Preview: read-only rendered title and body in the content scale, toolbar hidden.
- [ ] Offline: SaveStatus shows the wifi-off icon and reads "Waiting for connection", disabled
Save/Publish, OfflineBanner visible.
- [ ] Not found (404): the standalone error state, no editor chrome.
- [ ] Mobile (all of the above at 390×844): sticky bottom bar for Save/PublishControl, details
panel as a slideover, single "Edit article" mobile top-bar title instead of the breadcrumb.
- [ ] Keyboard/focus: Tab reaches the breadcrumb, Save, Preview switch and PublishControl in a
sensible order; Ctrl/⌘ S saves from anywhere in the editor surface; the leave-guard
ConfirmDialog traps focus starting on "Discard changes" per ConfirmDialog's a11y spec, and
Esc behaves like dismissing (equivalent to "Keep editing"); the character-counter live
region announces only when crossing the warn/over thresholds, not on every keystroke.
- [ ] Motion: modal open/close and skeleton pulse follow the standard 240 ms / 1.6 s timings and
stop under reduced motion.
## Manual verification
- **Real GET/PATCH round trip against a running server** — Vitest only mocks `useApi`; confirm
the real 200/404/401/500 responses and the real `sessionStorage` restoration across an actual
session expiry (`JWT_SECRET`-signed cookie timing out) against `task dev` with a live SQLite
file, since automated tests never exercise the real Go handler, `UpdateArticleUseCase`, or a
real cookie's `exp` claim.
- **Real `beforeunload` prompt** — Vitest can assert `onBeforeRouteLeave`'s ConfirmDialog, but a
real browser tab-close prompt (native `beforeunload`) needs manual confirmation, since jsdom
doesn't model it.