feat: home screen and navigation design
This commit is contained in:
1 parent
30a4df47b1
commit
930b836241
24 files changed
+1774
-19
No files matched your search
@@ -0,0 +1,101 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user