feat: article editor core
This commit is contained in:
1 parent
d493a181e1
commit
a4734b2c91
55 files changed
+4186
-496
No files matched your search
@@ -0,0 +1,242 @@
|
||||
Feature: Editor screen — load, edit and save an article's body
|
||||
|
||||
As a verified RenewCMS owner
|
||||
I want /article/:id/edit to load an article, let me edit its body, save it, and warn me
|
||||
before I lose unsaved work
|
||||
So that I can safely write and revise an article's content, with clear feedback while
|
||||
saving, near the length limit, offline, or when my session expires mid-edit
|
||||
|
||||
Background:
|
||||
Given /article/:id/edit is a verified route (meta.auth "verified") rendered as EditorPage
|
||||
inside DashboardShell, per docs/design/routes.md
|
||||
And EditorPage calls GET /v1/article/{id} once per navigation to load the article, per
|
||||
docs/design/screens/editor.md and features/public_article_read.feature
|
||||
And the header shows breadcrumb "Articles › {article.title}" linking "Articles" to
|
||||
/article (mobile top bar title "Edit article" instead), an ArticleStatusBadge size "md"
|
||||
reflecting article.is_online ("Draft" or "Online"), a SaveStatus span, a Save button,
|
||||
and a PublishControl button, per docs/design/components.md
|
||||
And the article's title is never editable after creation, per
|
||||
features/article_title_rules.feature; only the body is edited here
|
||||
And the body is edited in ArticleEditor (modelValue bound to the fetched HTML body); any
|
||||
change moves useArticleDraft's state from "clean" to "dirty", and PublishControl's label
|
||||
switches from "Publish" (or "Unpublish", if online) to "Save and publish" while dirty and
|
||||
the article is a draft; clicking PublishControl itself is out of scope for this slice and
|
||||
is specified in a follow-on slice (editor_publish_and_delete)
|
||||
And Save is enabled only when useArticleDraft's state is "dirty", the request is not already
|
||||
in flight, the app is not offline, and the body's HTML length is at most 10000 characters;
|
||||
it is disabled in every other case
|
||||
And Save (a click, or Ctrl/⌘ S) sends PATCH /v1/article/{id} with JSON {"body": "..."},
|
||||
the same contract features/edit_article_body.feature already gives the API: 200 with the
|
||||
updated article.Article, 400 for a body over 10000 characters, 404 for an id absent from
|
||||
the repository, 401 with no valid session
|
||||
And a successful save (200) sets useArticleDraft's state to "saved" and records the save
|
||||
time; SaveStatus shows "Saved at {HH:MM}" for the first hour and "Saved {n} min ago" after
|
||||
an hour has passed, per docs/design/components.md
|
||||
And a 500 response or a request that never reaches the server sets useArticleDraft's state
|
||||
to "error"; SaveStatus shows "Save failed" with an inline Retry button (UButton size="xs"
|
||||
variant="outline"); the edited body is kept exactly as typed, both in the editor and for
|
||||
the next Retry
|
||||
And a 404 response to a save (the article was deleted elsewhere) does not use the "error"
|
||||
SaveStatus; it shows a toast "That article no longer exists.", per docs/design/states.md's
|
||||
HTTP-status-to-copy table
|
||||
And a 401 response to a save is handled by useApi's global handling
|
||||
(features/api_client_global_error_handling.feature): before the redirect, useArticleDraft
|
||||
mirrors the current dirty body to sessionStorage keyed by the article's id, so it survives
|
||||
the redirect to /login?redirect=/article/{id}/edit&reason=expired
|
||||
And CharacterCounter shows "{len} / 10 000" below the editor, using the body's HTML string
|
||||
length (formatting and images count); at 9000 characters or more (90%, "Near the limit")
|
||||
it adds a warning icon and stays enabled; above 10000 ("Too long") it adds an icon and the
|
||||
text "{over} characters over. Shorten the text or remove an image. Formatting counts too.",
|
||||
and disables both Save and PublishControl until the length is 10000 or fewer again
|
||||
And a Preview switch, when on, replaces the editable ArticleEditor and its toolbar with a
|
||||
read-only rendering of the title and body in the content type scale; switching it off
|
||||
restores the editable surface with the same content, unchanged
|
||||
And when navigator.onLine is false, or the browser fires a window "offline" event,
|
||||
useArticleDraft's state becomes "offline"; SaveStatus shows the wifi-off icon and reads
|
||||
"Waiting for connection"; Save and PublishControl are disabled with a tooltip explaining
|
||||
why, and DashboardShell's existing OfflineBanner is shown; when the connection returns (a
|
||||
window "online" event, or navigator.onLine true again), Save and PublishControl re-enable
|
||||
according to the underlying dirty/saved/error state, which is unaffected by having been
|
||||
offline
|
||||
And navigating away (the breadcrumb "Articles" link, a sidebar nav link, the browser back
|
||||
button, or closing the tab) while useArticleDraft's state is "dirty" opens ConfirmDialog
|
||||
(neutral tone) titled "Leave without saving?", text 'Your changes to "{article.title}"
|
||||
haven't been saved.', cancel "Discard changes", confirm "Keep editing"; "Discard changes"
|
||||
proceeds with the navigation, discarding the edit; "Keep editing" closes the dialog and
|
||||
keeps the user on the editor with the edit intact; navigating away while "clean" or
|
||||
"saved" never opens this dialog
|
||||
And an id absent from the repository (GET resolves 404) replaces the entire editor with an
|
||||
error state: title "This article doesn't exist.", and a single action "All articles"
|
||||
navigating to /article; no breadcrumb-title, SaveStatus, Save, PublishControl or
|
||||
ArticleEditor renders in this state
|
||||
And skeleton placeholders (for the title, content and details) replace the editor's content
|
||||
only after 150 ms of GET /v1/article/{id} being unresolved, so a fast response never
|
||||
flashes a skeleton, per the Skeleton component spec
|
||||
|
||||
Scenario: Skeletons appear only after 150 ms, never for a fast response
|
||||
Given the user navigates to /article/12/edit for the first time in this session
|
||||
When GET /v1/article/12 has not resolved 150 ms after the navigation
|
||||
Then skeleton placeholders replace the title, content and details areas
|
||||
When GET /v1/article/12 then resolves 200 with a draft article titled "How we price a
|
||||
bathroom renovation"
|
||||
Then the skeletons are replaced by the Saved state's header, editor and details
|
||||
Given a second, separate navigation to /article/12/edit
|
||||
When GET /v1/article/12 resolves in under 150 ms
|
||||
Then no skeleton is ever shown, and the Saved state's content renders directly
|
||||
|
||||
Scenario Outline: Saved — a freshly loaded article, draft or online
|
||||
Given GET /v1/article/12 resolves 200 with an article titled "How we price a bathroom
|
||||
renovation", is_online <is_online>, body length 4318, updated_at "Today, 11:02"
|
||||
When the user navigates to /article/12/edit
|
||||
Then the breadcrumb reads "Articles › How we price a bathroom renovation"
|
||||
And the ArticleStatusBadge reads "<badge>"
|
||||
And SaveStatus reads "Saved at 11:02"
|
||||
And the Save button is disabled
|
||||
And PublishControl reads "<publish_label>"
|
||||
And the character counter reads "4 318 / 10 000"
|
||||
|
||||
Examples:
|
||||
| is_online | badge | publish_label |
|
||||
| false | Draft | Publish |
|
||||
| true | Online | Unpublish |
|
||||
|
||||
Scenario: Unsaved changes — any edit marks the article dirty
|
||||
Given the user is on /article/12/edit in the Saved state, article "How we price a bathroom
|
||||
renovation", draft
|
||||
When the user types into the body
|
||||
Then SaveStatus reads "Unsaved changes"
|
||||
And the Save button becomes enabled
|
||||
And PublishControl now reads "Save and publish"
|
||||
|
||||
Scenario: Saving — Save sends the PATCH and disables itself while in flight
|
||||
Given the user is on /article/12/edit with unsaved changes to the body
|
||||
When the user activates Save
|
||||
Then PATCH /v1/article/12 is sent with JSON {"body": "<the edited body>"}
|
||||
And SaveStatus reads "Saving…"
|
||||
And the Save button is disabled
|
||||
|
||||
Scenario: Ctrl/⌘ S saves the same way as clicking Save
|
||||
Given the user is on /article/12/edit with unsaved changes to the body
|
||||
When the user presses Ctrl+S (or ⌘+S)
|
||||
Then PATCH /v1/article/12 is sent with JSON {"body": "<the edited body>"}
|
||||
And SaveStatus reads "Saving…"
|
||||
|
||||
Scenario: A successful save returns to the Saved state with the new time
|
||||
Given the user is on /article/12/edit in the Saving state
|
||||
When PATCH /v1/article/12 resolves 200 with the updated article
|
||||
Then SaveStatus reads "Saved at {current time}"
|
||||
And the Save button is disabled again
|
||||
|
||||
Scenario: SaveStatus switches to a relative time after an hour
|
||||
Given the user is on /article/12/edit and PATCH /v1/article/12 resolved 200 at 11:02
|
||||
When more than an hour has passed with no further edit or save
|
||||
Then SaveStatus reads "Saved {n} min ago" instead of "Saved at 11:02"
|
||||
|
||||
Scenario Outline: Save failed — a 500 or a network failure keeps the changes and offers Retry
|
||||
Given the user is on /article/12/edit with unsaved changes to the body "Revised opening
|
||||
paragraph."
|
||||
When the user activates Save
|
||||
And PATCH /v1/article/12 <failure>
|
||||
Then SaveStatus reads "Save failed" with a Retry button
|
||||
And the editor still shows the body "Revised opening paragraph."
|
||||
And no navigation occurs
|
||||
|
||||
Examples:
|
||||
| failure |
|
||||
| resolves with a 500 response |
|
||||
| never reaches the server |
|
||||
|
||||
Scenario: Retry resends the same body and can recover
|
||||
Given the user is on /article/12/edit in the Save failed state, with unsaved body "Revised
|
||||
opening paragraph." and PATCH /v1/article/12 having failed with a 500
|
||||
When the user activates Retry
|
||||
Then PATCH /v1/article/12 is sent again with JSON {"body": "Revised opening paragraph."}
|
||||
When it resolves 200 with the updated article
|
||||
Then SaveStatus reads "Saved at {current time}"
|
||||
|
||||
Scenario: A save against an article deleted elsewhere shows a toast, not the error SaveStatus
|
||||
Given the user is on /article/12/edit with unsaved changes to the body
|
||||
When the user activates Save
|
||||
And PATCH /v1/article/12 resolves 404
|
||||
Then a toast reads "That article no longer exists."
|
||||
And SaveStatus does not read "Save failed"
|
||||
|
||||
Scenario: Near the limit — the counter warns at 9000 characters without blocking Save
|
||||
Given the user is on /article/12/edit with unsaved changes making the body's HTML length
|
||||
9240 characters
|
||||
Then the character counter reads "9 240 / 10 000" with a warning icon
|
||||
And the Save button remains enabled
|
||||
|
||||
Scenario: Too long — over 10000 characters disables Save and Publish until shortened
|
||||
Given the user is on /article/12/edit with unsaved changes making the body's HTML length
|
||||
10214 characters
|
||||
Then the character counter reads "10 214 / 10 000" with the text "214 characters over.
|
||||
Shorten the text or remove an image. Formatting counts too."
|
||||
And the Save button is disabled
|
||||
And PublishControl is disabled
|
||||
When the user shortens the body to 10000 characters or fewer
|
||||
Then the character counter no longer shows the over-limit text
|
||||
And the Save button is enabled again
|
||||
|
||||
Scenario: Preview shows the rendered title and body without the editing surface
|
||||
Given the user is on /article/12/edit, article titled "How we price a bathroom renovation"
|
||||
with some body content
|
||||
When the user switches Preview on
|
||||
Then the editing toolbar and the editable surface are replaced by a read-only rendering of
|
||||
the title and body in the content type scale
|
||||
When the user switches Preview off
|
||||
Then the editable ArticleEditor and toolbar return, with the same body content unchanged
|
||||
|
||||
Scenario: Offline blocks Save and Publish, and recovers when the connection returns
|
||||
Given the user is on /article/12/edit with unsaved changes to the body
|
||||
When navigator.onLine becomes false
|
||||
Then SaveStatus shows the wifi-off icon and reads "Waiting for connection"
|
||||
And the Save button and PublishControl are disabled with a tooltip explaining why
|
||||
When navigator.onLine becomes true again
|
||||
Then the Save button re-enables, reflecting the still-unsaved change
|
||||
And SaveStatus reads "Unsaved changes" again
|
||||
|
||||
Scenario: Leaving with unsaved changes prompts, and "Discard changes" proceeds
|
||||
Given the user is on /article/12/edit with unsaved changes to the body
|
||||
When the user activates the "Articles" breadcrumb link
|
||||
Then ConfirmDialog opens, titled "Leave without saving?", text 'Your changes to "How we
|
||||
price a bathroom renovation" haven't been saved.', cancel "Discard changes", confirm
|
||||
"Keep editing"
|
||||
When the user activates "Discard changes"
|
||||
Then the app navigates to /article
|
||||
And the edited body is not saved
|
||||
|
||||
Scenario: Leaving with unsaved changes, "Keep editing" stays on the page
|
||||
Given the user is on /article/12/edit with unsaved changes to the body, and the "Leave
|
||||
without saving?" dialog is open
|
||||
When the user activates "Keep editing"
|
||||
Then the dialog closes
|
||||
And the user remains on /article/12/edit with the unsaved body intact
|
||||
|
||||
Scenario: Leaving a clean article never prompts
|
||||
Given the user is on /article/12/edit in the Saved state, with no unsaved changes
|
||||
When the user activates the "Articles" breadcrumb link
|
||||
Then the app navigates to /article directly, with no ConfirmDialog
|
||||
|
||||
Scenario: Session expired mid-save keeps the draft and restores it after logging back in
|
||||
Given the user is on /article/12/edit with unsaved changes to the body "Paragraph added
|
||||
just before the session expired."
|
||||
When the user activates Save
|
||||
And PATCH /v1/article/12 resolves 401
|
||||
Then the body "Paragraph added just before the session expired." is mirrored to
|
||||
sessionStorage keyed by article id 12
|
||||
And the app navigates to /login?redirect=/article/12/edit&reason=expired
|
||||
When the user logs in again and is returned to /article/12/edit
|
||||
And GET /v1/article/12 resolves 200 with the last-saved body (without that paragraph)
|
||||
Then the editor shows the body "Paragraph added just before the session expired." restored
|
||||
from sessionStorage, not the last-saved body from the GET
|
||||
And SaveStatus reads "Unsaved changes"
|
||||
|
||||
Scenario: An unknown or deleted article id replaces the editor with an error state
|
||||
Given GET /v1/article/9999 resolves 404
|
||||
When the user navigates to /article/9999/edit
|
||||
Then the editor shows a single error state: title "This article doesn't exist." and one
|
||||
action "All articles"
|
||||
And no breadcrumb-title, SaveStatus, Save, PublishControl or ArticleEditor renders
|
||||
When the user activates "All articles"
|
||||
Then the app navigates to /article
|
||||
@@ -0,0 +1,129 @@
|
||||
# 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 character counter "4 318 / 10 000".
|
||||
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. **Near the limit** — set the body's HTML length to 9240 (via a fixture string of that
|
||||
length); assert counter "9 240 / 10 000" with a warning icon and Save still enabled.
|
||||
11. **Too long** — set the body's HTML length to 10214; assert counter "10 214 / 10 000" with
|
||||
"214 characters over. Shorten the text or remove an image. Formatting counts too.", Save
|
||||
disabled, PublishControl disabled; shorten to 10000; assert both re-enable and the over-limit
|
||||
text disappears.
|
||||
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
|
||||
"4 318 / 10 000", 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.
|
||||
- [ ] Near the limit: counter "9 240 / 10 000" with the warning icon/style.
|
||||
- [ ] Too long: counter "10 214 / 10 000" with the over-limit text and disabled Save/Publish.
|
||||
- [ ] 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.
|
||||
Reference in new issue
Block a user