Files
RenewCMS/features/editor_core.feature
T

233 lines
14 KiB
Gherkin
Raw 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.
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, and the app is not offline, whatever the body's length; 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 (any body length, including empty), 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 EditorStatusBar shows a neutral "{words} words · {characters} characters" count with
en-US thousands separators (e.g. "1,204 words · 7,318 characters", "0 words · 0 characters"
for an empty body); characters is the body's HTML string length, words are the
whitespace-separated tokens of its text content; the count has no limit, warning or error
styling, per features/article_body_unlimited.feature
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 count reads "{words} words · 4,318 characters"
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: A very long body keeps a neutral count and leaves Save and Publish enabled
Given the user is on /article/12/edit with unsaved changes making the body's HTML length
120000 characters
Then the count reads "19,845 words · 120,000 characters" with no warning styling
And the Save button is enabled
And PublishControl is enabled
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