Files

102 lines
7.4 KiB
Markdown

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