Files
RenewCMS/features/activity_screen.qa.md

20 KiB
Raw Permalink Blame History

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.