Files

7.4 KiB

QA: Home screen

Implementation note for coder (informational, not a QA assertion)

The greeting requires useApi.getMe() and the Session type/store (web/src/composables/useApi.ts, web/src/stores/session.ts) to also expose username, which GET /v1/auth/me already returns in its JSON body. Today getMe() only extracts is_verified and discards the rest, and Session is just { verified: boolean }. Widen MeResult and Session to carry username: string as well; this is a same-layer frontend response-type widening (the wire contract doesn't change), not a backend change.

Automated (Vitest + Vue Test Utils, task test:web)

Mock useApi's GET /v1/article/ call per case; no real backend, no httptest server. Mount HomePage inside DashboardShell with a resolved session (username "marta") and a test router at /home.

  1. Skeleton only after 150 ms — mock GET /v1/article/ with an unresolved promise; assert no skeleton is present before 150 ms elapse (use fake timers); advance past 150 ms; assert skeleton placeholders replace the stat tiles and the 5 recent-list rows, and the list container has aria-busy="true"; resolve the promise with 200 and a non-empty list; assert the skeletons are gone and the Default content is shown. Separately, mock a promise that resolves in under 150 ms; assert no skeleton ever renders.
  2. Default state — mock GET /v1/article/ resolving 200 with the 6-article fixture (4 online, 2 drafts, ordered most-recently-updated first as in the feature); assert subtitle "Here's where things stand on your site."; assert stat tiles "6" "articles", "4" "online", "2" "drafts"; assert the recent list shows exactly the first 5 articles in order, each with its ArticleStatusBadge and updated date, each row linking to /article/:id/edit; assert the 6th (least recently updated) article is absent from the recent list; assert the PageHeader's "New article" action opens NewArticleModal.
  3. Empty, first run — mock GET /v1/article/ resolving 200 with []; assert subtitle "Welcome to RenewCMS."; assert no stat tiles render; assert an empty state renders in place of the recent list with icon file-text, title "Write your first article", text "It starts as a draft, so nothing appears on your site until you publish it.", and action "New article"; click it; assert NewArticleModal opens.
  4. Error state — Examples-driven: (a) GET /v1/article/ resolves 500, (b) it rejects with a network error. In each: assert the breadcrumb, greeting and subtitle remain visible and the subtitle stays "Here's where things stand on your site."; assert no stat tiles or recent list render; assert an inline alert with role="alert" renders in their place, with icon circle-x, title "Couldn't load your articles", text "Check your connection and try again.", and a single action "Try again"; assert the alert text is identical between the 500 case and the network- failure case — there is no separate wording per cause.
  5. Retry recovers into Default — start from the Error state (mocked 500); click "Try again"; assert GET /v1/article/ is called a second time; resolve it 200 with the 6-article fixture; assert the Alert is replaced by the stat tiles "6"/"4"/"2" and the recent list.
  6. Single request per navigation — mock GET /v1/article/ resolving 200 with the 6-article fixture; mount /home; assert the mock was called exactly once, and both the stat tiles and the recent list read from that one resolved value (no second call fires for either).
  7. Greeting matches the hour of day, at every state, for the session username — stub the clock (e.g. vi.setSystemTime or a mocked Date.now/Date.prototype.getHours) to each boundary hour in turn (0, 11, 12, 17, 18, 23) and mount with session username "marta"; assert the greeting above the subtitle reads exactly "Good morning, marta" for hours 0 and 11, "Good afternoon, marta" for hours 12 and 17, and "Good evening, marta" for hours 18 and 23. Then, fixing the hour at 9 ("Good morning, marta"), assert the same exact greeting text is shown above the subtitle in all four states: while GET /v1/article/ is unresolved past 150 ms (Loading), once it resolves with the 6-article fixture (Default), once it resolves with [] (Empty), and once it rejects with a 500 (Error).
  8. "Connect your website" card renders in every state — assert the static card (icon plug, title "Connect your website", text "Your site reads published articles from RenewCMS. Share this guide with whoever builds it.", link "Open the guide" navigating to /integration) is present and identical while GET /v1/article/ is unresolved past 150 ms (Loading), once it resolves with the 6-article fixture (Default), once it resolves with [] (Empty), and once it rejects with a 500 (Error). The card is not part of the PageHeader's actions.

Visual (manual verification — desktop and mobile, light and dark)

Reference: docs/design/screens/home.md and docs/design/source/RenewCMS Screens Back-office.dc.html, group "Home". Run task visual:check -- home for each state and compare against the board frame.

  • Loading state: skeleton stat tiles and 5 skeleton recent rows, shaped like the real layout, matching the board.
  • Default state: stat tiles, recent list with ArticleStatusBadge per row, PageHeader primary action "New article", breadcrumb "Home", and the "Connect your website" card below — matches the board exactly at desktop and mobile.
  • Empty, first run state: the empty state replaces the stats and recent list, matching the board's icon tile, title "Write your first article", text, and single "New article" action.
  • Error state: the inline alert's icon (circle-x), title "Couldn't load your articles", text "Check your connection and try again.", and "Try again" action match the board; the breadcrumb, greeting and subtitle stay visible above it.
  • "Connect your website" card: icon plug, title, text and "Open the guide" link match the board, and the card renders identically in every state (Loading, Default, Empty, Error).
  • Greeting placement: the greeting sits above the subtitle in the PageHeader area (not the note board's fixture text, since the greeting is new content — position and type scale should read as a lightweight line above the description, not competing with the h1).
  • Keyboard/focus: Tab order reaches "New article" in the header, then the "Connect your website" card's "Open the guide" link, before the recent list; the Error state's "Try again" button is reachable and, on activation, focus stays sensible (not lost) while the request re-runs; the recent list's rows are reachable and each activates its /article/:id/edit link with Enter.
  • Motion: the skeleton pulse runs at 1.6 s and stops under reduced motion, per the Skeleton component spec.

Manual verification

  • Real GET /v1/article/ against a running server — Vitest only mocks useApi; confirm the real 200/500 round-trip and the real article ordering (by updated_at) against task dev with a live SQLite file, since these automated tests never exercise the actual Go handler or the ListArticles use case.
  • Real session username on a live app-boot — confirm GET /v1/auth/me's username field flows through the real session store into the greeting, since Vitest only ever supplies a mocked session.