# 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.