feat: article creation modal
This commit is contained in:
1 parent
a4734b2c91
commit
c47d33eaa9
8 files changed
+1030
-116
No files matched your search
@@ -0,0 +1,97 @@
|
||||
# QA: Creating a new article from NewArticleModal
|
||||
|
||||
## Implementation note for coder (informational, not a QA assertion)
|
||||
|
||||
Only HomeView.vue is wired as a real `created` consumer in this slice: it listens for
|
||||
`@created="article => { router.push(...); useToast().add(...) }"`. The Articles-page trigger and
|
||||
the `/article/new` route stay pointed at the current ArticleView.vue placeholder until that page
|
||||
exists; don't wire a `created` handler there yet.
|
||||
|
||||
The confirm button's label changes from the placeholder's "Create article" to the design's
|
||||
"Create draft" — this is a deliberate copy change, not a leftover.
|
||||
|
||||
The Created toast uses `useToast().add({ id: 'article-created-{id}', color: 'success', icon:
|
||||
'i-lucide-circle-check', title: 'Draft created', description: '"{title}" is saved as a draft.' })`,
|
||||
per docs/design/components.md's toast-id convention (action + article id) and the Toast rules
|
||||
section (success/info close after 5 s).
|
||||
|
||||
## Automated (Vitest + Vue Test Utils, `task test:web`)
|
||||
|
||||
Mock `useApi`'s `request` for `POST /v1/article/` per case; no real backend, no httptest server.
|
||||
Mount HomePage (which owns NewArticleModal) inside DashboardShell with a resolved verified session,
|
||||
and a test router at `/home`. Sample data throughout: title "Choosing tiles for a small bathroom",
|
||||
first paragraph "Small rooms show every grout line, so we start with the floor.", created article
|
||||
id 42, is_online false.
|
||||
|
||||
1. **Default state on open** — click "New article"; assert the modal shows title "New article",
|
||||
the intro text, an empty focused Title field with counter "0 / 200" and help "3 to 200
|
||||
characters.", an empty First paragraph field with its placeholder and help, "Cancel" and
|
||||
"Create draft".
|
||||
2. **Blur validation, Title** — type "Ti" into Title and blur; assert the error "Use at least 3
|
||||
characters." replaces the help text and the counter "2 / 200" is in its error style; assert no
|
||||
`request` call.
|
||||
3. **Blur validation, First paragraph** — with a valid Title, leave First paragraph empty and
|
||||
blur; assert "Add a first paragraph. It can be a single sentence." appears; assert no
|
||||
`request` call.
|
||||
4. **Submit with both fields invalid** — Title "Ti", empty First paragraph, click "Create draft";
|
||||
assert both errors show and `request` is never called.
|
||||
5. **Submitting state** — fill both fields validly, click "Create draft"; assert `request` is
|
||||
called once with `POST /v1/article/` and the exact JSON body; assert both fields and Cancel
|
||||
become disabled, the confirm button reads "Creating…"; assert pressing Escape and clicking the
|
||||
backdrop do not close the modal while unresolved.
|
||||
6. **Successful create hands off** — from Submitting, resolve `request` 201 with the fixture
|
||||
article (id 42); assert the modal closes; assert the router navigated to `/article/42/edit`;
|
||||
assert a toast was raised with id "article-created-42", color success, icon
|
||||
"i-lucide-circle-check", title "Draft created", and description "\"Choosing tiles for a small
|
||||
bathroom\" is saved as a draft."
|
||||
7. **Title rejected (400)** — from Submitting, resolve `request` rejecting with `ApiError(400)`;
|
||||
assert both field values are kept, the Title field shows "Check this title and try again.",
|
||||
an alert reads "Something in the form isn't valid. Check the highlighted fields.", and the
|
||||
First paragraph field shows no error.
|
||||
8. **Error, Examples-driven** — from Submitting, (a) resolve `request` rejecting with
|
||||
`ApiError(500)`, (b) resolve it rejecting with `NetworkError`; in each, assert both field values
|
||||
are kept, the alert reads "Couldn't create the article. Your text is still here. Try again in a
|
||||
moment.", and neither field shows an error.
|
||||
9. **Retry after Error succeeds** — from the Error state (500), click "Create draft" again; assert
|
||||
`request` is called a second time with the same JSON body; resolve 201 with the fixture; assert
|
||||
the modal closes, the router navigated to `/article/42/edit`, and the same "Draft created" toast
|
||||
(id "article-created-42") is raised.
|
||||
10. **Cancel discards** — fill both fields, click "Cancel"; assert the modal closes immediately
|
||||
with no confirmation UI, and `request` was never called.
|
||||
|
||||
## Visual (manual verification — desktop and mobile, light and dark)
|
||||
|
||||
Reference: `docs/design/screens/new-article.md` and `docs/design/source/RenewCMS Screens
|
||||
Back-office.dc.html`, group "New article". Run `task visual:check -- new-article` for each state
|
||||
below and compare against the board frame; add each state's mock setup to `web/visual/states.ts`
|
||||
first, reusing this slice's sample data (title "Choosing tiles for a small bathroom", first
|
||||
paragraph "Small rooms show every grout line, so we start with the floor."). This screen has no
|
||||
time-dependent content, so no fixed `now` is needed.
|
||||
|
||||
- [ ] Default: empty, focused Title with counter "0 / 200", empty First paragraph, help text on
|
||||
both fields, "Cancel" / "Create draft".
|
||||
- [ ] Validation: Title "Ti" with counter "2 / 200" in error and "Use at least 3 characters.";
|
||||
First paragraph error "Add a first paragraph. It can be a single sentence."
|
||||
- [ ] Submitting: both fields filled and disabled, confirm reads "Creating…" with its loading
|
||||
state.
|
||||
- [ ] Title rejected (400): both fields filled, Title error "Check this title and try again.",
|
||||
alert "Something in the form isn't valid. Check the highlighted fields."
|
||||
- [ ] Error: both fields filled, alert "Couldn't create the article. Your text is still here. Try
|
||||
again in a moment.", no field-level error.
|
||||
- [ ] Created: no board frame; verified by Vitest (toast copy + navigation).
|
||||
- [ ] Keyboard/focus: Tab reaches Title (already focused on open) → First paragraph → Cancel →
|
||||
Create draft; Esc closes the modal in every dismissible state and is blocked while
|
||||
Submitting; the counter's live region announces only when its error/warn threshold is
|
||||
crossed, not on every keystroke, per CharacterCounter's a11y spec.
|
||||
- [ ] Motion: modal open/close follows the standard 240 ms timing and stops under reduced motion.
|
||||
|
||||
## Manual verification
|
||||
|
||||
- **Real POST /v1/article/ round trip against a running server** — Vitest only mocks `useApi`;
|
||||
confirm the real 201/400/500 responses and that a genuinely-created article is retrievable at
|
||||
`/article/{id}/edit` against `task dev` with a live SQLite file, since automated tests never
|
||||
exercise the real Go handler or `CreateArticleUseCase`.
|
||||
- **Toast placement and stacking on a live app** — confirm the "Draft created" toast's real
|
||||
position, duration and stacking behavior (per docs/design/components.md's Toast rules) against
|
||||
a running app, since Vitest only asserts that `useToast().add` was called with the right
|
||||
arguments, not the rendered/animated toast region.
|
||||
Reference in new issue
Block a user