Files
RenewCMS/features/activity_screen.qa.md

252 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QA: Activity screen, details slide-over and My activity
## Implementation note for coder (informational, not a QA assertion)
The activity API is already built and covered by features/activity-log-backend.qa.md and
features/activity-log-producers.qa.md: this slice has no Go change and no HTTP-level Go section.
The shell specs that list the navigation are amended to seven destinations (Home, Articles, Media,
Users, Activity, Integration, Settings): features/dashboard_shell_branding.feature,
features/media_browse.feature, features/users_screen.feature and .qa.md,
features/settings_page.feature and .qa.md, features/profile_page.feature. Their Vitest tests
(DashboardShell.spec.ts, where Settings moves from index 5 to 6) must be updated in the same change.
Export needs the response body as a file, which `useApi().request` does not return (it calls
`response.json()`). Add a raw or blob read to `useApi` that keeps the global 401 handling, builds
an object URL and clicks a temporary anchor with `download="activity.csv"`. The toasts use
`useToast().add({ color, icon, title })` with the one sentence as the title.
The page needs a date-range popover and a Custom date pair in the mobile sheet. The clamp ("Show
more") is measured from the rendered element, so tests stub `scrollHeight` and `clientHeight`.
Every new English string needs a French entry in `web/src/i18n/messages.ts`. Sentences are
composed by a composable (`useActivitySentence`) that returns segments so the target name can be
rendered semibold. The Security sub-line is "{n} flagged" with n = `summary.security`, French
"{n} signalés".
## Automated (Vitest + Vue Test Utils, `task test:web`)
Mock `useApi`'s `request` (and the export transport) per case, matching on method, path and parsed
query (`URLSearchParams`); no real backend. Mount ActivityView inside DashboardShell with a
resolved verified session (id 2, username "ines", display_name "Inès Duarte", role "owner") and a
test router at /activity; the regular-user cases use role "editor" (and one with no role). Run
each layout case at a desktop and a below-md width (mock `matchMedia`, see
`components/__tests__/viewport.ts`). Timers: use `vi.useFakeTimers({ toFake: ['setTimeout',
'clearTimeout'] })` so the 150 ms skeleton delay and the 300 ms search debounce can be advanced
while `Date` stays real. No test freezes or asserts the system date.
Clock-independence rules for fixtures: every event in the Vitest fixtures is dated 3 Oct 2026 or
earlier, created at 12:00 to 13:00 UTC so its local day is the same in any time zone from UTC-11
to UTC+11; assert day separators with the absolute label "3 Oct 2026" and When cells only against
the pattern `^\d{2}:\d{2}$` and a non-empty `title`; never assert "Today", "Yesterday" or a
relative "N min ago". UTC lines can be asserted exactly; the local time line only with its
"(your time)" suffix.
Fixtures: `activityItems` (a 15-item list built from the board's events, mapped to the API shape
with the action codes of the "Visual" section), `summaryDefault` (total 1284, previous_total
1146, active_people {users 7, api_keys 2}, failed 6, security 11, series total [4,6,5,8,7,9,6],
active [5,6,6,7,7,8,9], failed [0,1,0,2,1,0,2], security [1,0,2,1,3,2,2]), `summaryZero` (total
214, previous_total 223, active_people {4, 1}, failed 0, security 0, series failed and security
zeros) and `summaryNone` (all zeros). The list response is `{ items, total, page, size }`.
1. **Navigation** — sidebar and drawer list Home, Articles, Media, Users, Activity, Integration,
Settings in order; "Activity" has the `i-lucide-history` icon, links to /activity, has
aria-current="page" on /activity and none elsewhere; breadcrumb and mobile title "Activity".
2. **Default (owner)** — assert header texts, enabled "Export", the four strip cards (labels,
"1,284", "9", "6", "11", sub-lines "12% vs previous 7 days" with the trending-up icon, "7
people, 2 API keys", "0.5% of all actions" with icon `circle-x`, "11 flagged" with icon
`shield-alert`), the filter bar (placeholder, "All people", "All categories", Outcome All
selected, 7 days selected), no chip, the pager text "Showing 1–25 of 1,284", and that `request`
was called exactly twice at mount with the expected queries (`range=7d&page=1&size=25` and
`range=7d`).
3. **Rows, Examples-driven** — one fixture row per Examples row of the feature; assert the
Actor cell (initials, key or cog or user-round-x icon, badge, "Not signed in"), the sentence
text and that the target name is in a semibold element, the shield marker only on security
rows, and the Outcome badge word.
4. **Target cell** — deleted (struck-through class, "Deleted" badge, type line), living (link
with `href` "/article/8/edit", "/media", "/users", "/integration/api-keys" per type, plain text
for "release"), and null ("—"); clicking the link navigates and does not open the details.
5. **Day grouping and When cell** — fixture across 3 Oct and 2 Oct 2026; assert two separators
"3 Oct 2026" and "2 Oct 2026" in order, the When cell pattern and `title`.
6. **Sentences, Examples-driven** — for each Examples row set the locale (`setLocale`), render
and assert the exact sentence, including plurals, `failed_groups`, "Forever", the dropped slot
for a missing target name, the unknown code `article.archive`, and the success sentence used
for an unlisted failed action (`auth.login` failed). Add a completeness test: every action in
the catalogue has an English and a French template, and every key used on the page exists in
`messages.ts`.
7. **Loading** — unresolved requests; at 149 ms nothing in place of strip and table; at 150 ms 4
skeleton cards, 9 skeleton rows (5 cards below md), `aria-busy="true"`, no pager; resolve and
assert the Default content.
8. **Zero-value strip** — `summaryZero`; assert "214" "4% vs previous 7 days" with the
trending-down icon, "5" "4 people, 1 API key", "0" "Nothing failed" and "0" "Nothing to flag"
with `circle-check`, no error or warning fill class, bars at 2 px.
9. **Total sub-line without a comparison, Examples-driven** — previous_total 0 gives "No earlier
data" and equal totals give "No change", both without a trend icon; French "Aucune donnée
antérieure" and "Aucun changement"; ranges Today, 30 days and Custom give "vs previous day",
"vs previous 30 days" and "vs previous period".
10. **Empty, no activity yet** — `summaryNone`, items []; assert "No data yet" on two cards, the
calm lines on the other two, the empty panel copy and icon, a disabled "Export", no table
header and no pager.
11. **Empty, filters match nothing** — type "sitemap", choose Category "Media" and Outcome
"Failed", resolve items []; assert chips, empty panel copy, "Clear filters", enabled export;
click "Clear filters" and assert a request with `range=7d&page=1&size=25` only.
12. **Error, Examples-driven** — list rejects with `ApiError(500)`, `ApiError(400)`, then
`NetworkError`, summary resolves; assert the strip, the alert copy, "Try again", a disabled
export and no table; click "Try again"; assert the list request is repeated and the Default
rows replace the alert.
13. **Summary alone fails** — summary rejects with `ApiError(500)`, `ApiError(403)`, then
`NetworkError`; assert the strip is absent, nothing is drawn in its place, and the list is
fully rendered.
14. **Forbidden** — list rejects with `ApiError(403)` and a verified session; assert no export
button, no strip, no filter bar, the lock panel copy, and that "Back to home" navigates to
/home. Also `ApiError(401)` triggers the global redirect and no panel.
15. **Strip cards, Examples-driven** — click each card; assert the effect and `aria-pressed`
(Failed: request with `outcome=failed&page=1`, chip "Outcome: Failed"; Security: request with
`security=true`, chip "Security events"; Total: filters cleared; Active people: no request)
and that the summary is not requested again.
16. **Filtered, card applied** — Category "Authentication", 30 days, then the Failed card; assert
the chip texts and order, "Clear all", the summary requested only once for `range=30d`, the
pager "Showing 1–6 of 6", and below md the Filters badge "3".
17. **Filter bar, Examples-driven** — for each Examples row assert the exact query, `page=1` after
starting on page 3, and the 300 ms debounce with fake timers (no request at 299 ms, one at
300 ms; Enter applies at once and the pending debounce does not send a second request).
18. **Custom dates** — pick two dates in the popover (stub the calendar's model); assert no request
after the first date, then `range=custom&from=2026-09-01&to=2026-09-30` on the list and the
summary, and the chip "Dates: 1 – 30 Sep 2026"; also "28 Sep – 3 Oct 2026".
19. **Chips** — remove one chip, then "Clear all"; assert the queries and that the summary is
requested again only when the range changes back.
20. **Person options** — assert no `GET /v1/user/` or `GET /v1/api-key/` call at mount, both on
first open of the menu, none on the second; assert group order and the three query values.
21. **Pagination** — next page, size 50, last page "Showing 1,276–1,284 of 1,284" with next
disabled; mobile "Page 1 of 52" and previous and next.
22. **Mobile list and sheet** — below md assert cards (actor line, sentence, badges, "Target
deleted"), "Filters" with the badge count, the sheet content and footer ("Show 6 results", "Show
1 result"), two date fields for Custom, and that "Show … results" closes it.
23. **Export** — superseded by activity_filter_cards_and_export.qa.md (menu, dialog, toasts).
24. **Export failure** — superseded by activity_filter_cards_and_export.qa.md.
25. **Details open and close** — click, then Enter on a focused row; assert the title, the selected
row class, the footer "Event 2 of 1,284"; close with Escape, the close button and the scrim and
assert focus on the row.
26. **Details content, Examples-driven** — the six board events: Edit with a diff (Title row with
both values and the "Body edited (+312 characters)" row, "(+1 character)", "(-48 characters)"),
long text (stub overflow; "Show more" on that row only, toggles to "Show less"), failed with a
long title (alert "Why it failed", wrapping), deleted target (struck-through name, "Deleted"
badge, explanation, no Changes), system event ("System" badge, "System task", two "Not
applicable"), failed login ("Not signed in", "Security" badge, reason, IP and Browser). Assert
the field labels and the Draft and Published values for `online`, the raw code for an unknown
field, and that no body text is ever rendered.
27. **Details times** — `created_at` "2026-10-06T10:42:18Z": the UTC line is exactly "6 Oct 2026,
10:42:18 UTC" and the local line ends with "(your time)".
28. **Previous and Next across pages** — first item: previous disabled, "Event 1 of 1,284"; 24
nexts without a request; the 25th next sends `page=2&size=25` with the same filters, shows
page 2's first item, "Event 26 of 1,284" and the pager "Showing 26–50 of 1,284"; previous
sends `page=1` and shows "Event 25 of 1,284"; the last event disables next; a 500 on the
neighbour page keeps the panel on the current event and re-enables the button; the position
also follows a non-default size (size 50, page 2, index 0 is "Event 51").
29. **Details below md** — the bottom-sheet variant: header "2 of 1,284", full-width buttons, no
footer text.
30. **My activity, Examples-driven** — role "editor" then no role: title, subtitle, breadcrumb "My
activity", no strip, no Person filter, no export, no Actor column or card actor line; assert no
request to `/summary` or `/export` and no `actor` parameter; pager "Showing 1–25 of 41"; empty
state copy; details "Event 1 of 41" with previous disabled.
31. **French** — `setLocale('fr')`; assert the chrome strings (Activity "Activité", My activity
"Mon activité", Export CSV "Exporter", Preparing file… "Préparation du fichier…",
Total actions "Actions au total", Active people "Personnes actives", Failed actions "Actions
en échec", Security events "Événements de sécurité", Succeeded "Réussie", Failed "Échec", Not
signed in "Non connecté", Activity details "Détails de l'activité", Previous event and Next
event "Événement précédent" and "Événement suivant", "11 signalés"), the French sentences, the
plural and "Forever" ("Toujours") forms, and the day separator format.
32. **Hover time** — each When cell has a `title` that is non-empty and contains seconds.
## Visual (task visual:check -- activity, desktop and mobile, light and dark)
Reference: `docs/design/screens/activity.md`, board `docs/design/source/RenewCMS Screens
Activity.dc.html`, groups "Activity, owner and admin", "Details slide-over" and "My activity"
(the last two are already declared as `extraSources` in `web/visual/check.ts`); exact values in
`docs/design/source/ActivityFrame.dc.html`. Add each state to `visualStates.activity` in
`web/visual/states.ts` with the board's own sample data. The gate is hard: every capture of every
non-skipped state must be within 10 %, for both viewports and both themes (6 gated states, 24
captures). No state uses the `now` option or a frozen clock. Mocks are keyed by method and path
(`GET /v1/activity`, `GET /v1/activity/summary`, `GET /v1/auth/me`); use `bodyFor` to read the
query. Do not mock or call `/v1/user/` or `/v1/api-key/` (Person options load only when the menu
opens, which no state does), so there is no unmocked request.
Sessions: add `activityOwnerSession` (id 2, display_name "Inès Duarte", role "owner") and
`activityEditorSession` (id 3, display_name "Marta Alves", role "editor"), as the board draws them
in its sidebar chip. The board draws a 4-item sidebar for the regular user as illustrative; the
real shell keeps all 7 items, a known small difference in the My activity Empty capture.
Fixtures (all taken from the frame's script): `activityBoardEvents`, the 15 events of the frame in
its order, mapped to API items. Where the frame's sample has no real producer the nearest real
code is used and the sentence differs from the board (accepted; those rows only appear in skipped
states): 1 `article.publish`, 2 `article.edit` with title and body (+312) changes, 3
`auth.login_failed` (anonymous, name "alex@example.com", reason from the frame), 4
`api_key.revoke`, 5 `system.media_purge` (system, ip and user_agent null), 6 `invite.accept`
(replaces "Changed the role"), 7 `media.upload` by the API key "Website sync", 8 `media.delete`
(target not existing), 9 `article.publish` failed with the 150-character title, 10
`system.activity_cleanup`, 11 `article.edit` with long title values (replaces the "Site
description" setting diff), 12 `api_key.create`, 13 `user.update_profile`, 14 `media.upload`, 15
`auth.password_reset_completed`; times are the frame's local times minus one hour as UTC
(`created_at`), IPs and browsers as in the frame, security as in the frame. Summary fixtures are
the three in the Automated section; the strip of the Default state is "1,284 / 9 / 6 / 11" with
the sub-line "11 flagged". Total is 1284 with page size 25 for the owner and 41 for the editor.
- [ ] Loading: both requests with `hang: true`; the setup waits 150 ms or more so the skeleton
strip and rows are drawn before the capture.
- [ ] Empty, no activity yet: list `items: [], total: 0`, summary `summaryNone`; "Export"
disabled.
- [ ] Empty, filters match nothing: the board applies Search "sitemap", Category "Media", Outcome
"Failed" (steps type and select them, or the mock answers with the filtered query), list
`items: [], total: 0`, summary Default.
- [ ] Error with retry: list 500, summary Default; the alert, disabled export, strip drawn.
- [ ] Forbidden (403): list 403, summary 403; session with a non-admin role as the board does
(`role: 'user'`), so no "Export".
- [ ] My activity, Empty: `activityEditorSession`, list `items: [], total: 0`.
- [ ] skip: clock-sensitive. Default, large dataset: `skip: 'clock-sensitive'` (day separators and
"2 min ago" depend on the real clock). Prints SKIP, never investigated or date-frozen.
- [ ] skip: clock-sensitive. Zero-value summary strip, Filtered card applied and Mobile filters
sheet (all draw Today rows behind the strip or sheet).
- [ ] skip: clock-sensitive. The six Details slide-over states (Edit with a diff, Settings diff
long text, Failed action very long target title, Deleted target no diff, System-initiated no
human actor, Security event failed login) and the My activity states Default and Details:
the rows behind the scrim show relative times and the panel shows the browser's local
time. Their Vitest coverage is cases 3, 5, 25 to 30.
- [ ] Layout, from `ActivityFrame.dc.html`, not `components.md`: header row is the title block
(20 px/28 px title) on the left and the 32 px (44 px mobile) "Export" button on the right,
aligned to the bottom; the strip is 4 columns with a 12 px gap on desktop and a clipped row
of 156 px cards with an 8 px gap on mobile; the table header is 36 px, day rows 32 px, rows at
least 52 px; the slide-over is 480 px wide on desktop, the bottom sheet 88 % high.
- [ ] Error is drawn as the frame's alert (error soft background, circle-x, title, text, 28 px
outline "Try again"), the empty states as the frame's centred panel (44 px icon box, 15 px
semibold title, muted text up to 340 px).
- [ ] Not covered by screenshots: keyboard and focus order (Tab reaches Export CSV, the strip
cards, the search, the selects, the groups, a row; Enter opens the details; focus is trapped
in the slide-over and returns to the row on close; Escape closes it); hover (the row, the
native `title` tooltip), the 240 ms slide-over and sheet motion and the skeleton pulse, all
stopped under reduced motion; the date-range popover.
## Manual verification
- **Real round trips against `task dev`** — Vitest only mocks `useApi`. As owner: perform an
article publish, an image upload and a failed login, then confirm they appear newest first with
the right sentences, the shield marker on the failed login, the Succeeded and Failed badges, the
summary numbers and the sparkline bars; apply each filter and each card; page through with size
25, 50 and 100; open the details across a page boundary.
- **Real CSV** — click "Export" with and without filters and open `activity.csv`: columns
`when,actor,action,target,outcome,source,ip`, the filtered rows only, no page or size applied,
neutralised cells that start with `=`, `+`, `-` or `@`; confirm the browser download prompt and
the two toasts.
- **Admin session** — sign in as an admin: the same full view as the owner.
- **Time zones** — set the OS time zone to UTC-8 and UTC+9, reload, and confirm the day
separators, the clock times, the "(your time)" line and the UTC line are consistent.
- **Regular user** — no real account can reach "My activity" today; confirm by a mocked
`/v1/auth/me` in the browser (or a future role) that the view has no strip, no Person filter and
no export, and that the server answers 403 for `actor=user:2` and for the summary and export.
- **API key** — calling `GET /v1/activity` with an API key answers 403 "This action needs a real
session." and the SPA never does it.
- **"Today", "Yesterday" and relative times** — confirm them against the real clock, since
visual:check skips every state that shows them.
- **French** — switch the language and read every state, the sentences and the toasts.
- **Real screen readers and zoom** — row selection, the sparkline buttons (labelled by the card
text), the sheet focus trap.