Files
RenewCMS/features/new_article_modal.qa.md
T

6.3 KiB

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 modal is title-only: the First paragraph field is removed and the POST body is {"title": "..."} with no body key (see features/article_body_unlimited.feature).

The confirm button's label is the design's "Create draft".

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", 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.", "Cancel" and "Create draft", and no First paragraph field.
  2. Blur validation — 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. Submit with an invalid title — Title "Ti", click "Create draft"; assert the error shows and request is never called.
  4. Submitting state — fill Title validly, click "Create draft"; assert request is called once with POST /v1/article/ and the JSON body exactly {"title": "Choosing tiles for a small bathroom"}; assert Title and Cancel become disabled, the confirm button reads "Creating…"; assert pressing Escape and clicking the backdrop do not close the modal while unresolved.
  5. 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."
  6. Title rejected (400) — from Submitting, resolve request rejecting with ApiError(400); assert the title is kept, the Title field shows "Check this title and try again.", and an alert reads "Something in the form isn't valid. Check the highlighted fields."
  7. Error, Examples-driven — from Submitting, (a) resolve request rejecting with ApiError(500), (b) resolve it rejecting with NetworkError; in each, assert the title is kept, the alert reads "Couldn't create the article. Your text is still here. Try again in a moment.", and the field shows no error.
  8. 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.
  9. Cancel discards — fill Title, 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"). This screen has no time-dependent content, so no fixed now is needed. The board frames may still draw a body line in Submitting, Title rejected and Error; the design .md files are authoritative for the title-only modal and the boards are a known follow-up.

  • Default: empty, focused Title with counter "0 / 200", help text, "Cancel" / "Create draft".
  • Validation: Title "Ti" with counter "2 / 200" in error and "Use at least 3 characters."
  • Submitting: Title filled and disabled, confirm reads "Creating…" with its loading state.
  • Title rejected (400): Title filled, error "Check this title and try again.", alert "Something in the form isn't valid. Check the highlighted fields."
  • Error: Title 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) → 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 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, that a title-only create returns 201 with an empty body, and that the 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.