Files
RenewCMS/features/profile_page.qa.md
T

170 lines
13 KiB
Markdown

# QA: Profile page, display name and the account menu
## Implementation note for coder (informational, not a QA assertion)
There is no Go change and no HTTP-level Go section: PATCH /v1/user/me, GET /v1/auth/me and POST
/v1/auth/password-reset are already covered (features/user_display_name.qa.md,
features/get_current_user.qa.md, features/request_password_reset.qa.md).
Amended specs (their Vitest tests must be updated in the same change): dashboard_shell_branding.feature
(UserMenu label, initials, groups and the collapsed aria-label), home.feature (greeting name),
plus session.spec.ts, useApi.spec.ts (getMe maps email and display_name, absent means ""),
UserMenu.spec.ts, DashboardShell.spec.ts and the HomeView spec.
The Saved toast uses `useToast().add({ id: 'display-name-saved', color: 'success', icon:
'i-lucide-circle-check', title: 'Display name saved', description: 'You now appear as {name}
across RenewCMS and in emails.' })`. The 429 countdown reuses `useRetryAfter`; the 300 s fallback
applies when `ApiError.retryAfter` is undefined or 0.
## Automated (Vitest + Vue Test Utils, `task test:web`)
Mock `useApi`'s `request` and `getMe` per case; no real backend. Mount ProfilePage inside
DashboardShell with a resolved verified session (id 1, username "marta", email
"marta@renew.example", displayName "") and a test router at /profile. Use fake timers only for the
countdown. No test freezes the system date; the Home greeting cases mock the hour as
home.qa.md does and assert the name part only. Run the layout cases at a desktop and a
below-lg width (mock `matchMedia`).
1. **Route guards, Examples-driven** - verified renders; unverified redirects to
/register/pending; 401 redirects to /login?redirect=/profile&reason=expired.
2. **Default** - breadcrumb "Profile", title, subtitle, empty field with placeholder, "0 / 60",
footer "You appear as “marta”, your username.", Save disabled, Username and Email inputs
disabled with their values, Password text with the email, "Send reset link", no note; no
nav item has aria-current.
3. **Loading** - unresolved GET /v1/auth/me; assert three skeleton cards, aria-busy, no AppBoot;
resolve and assert Default.
4. **From the menu** - navigate from /home via "Profile"; assert no skeleton and no second
GET /v1/auth/me.
5. **Editing** - type "Marta Alves"; assert "11 / 60", "Unsaved changes", Save enabled, no request.
6. **Counter, Examples-driven** - each table row, including the 60/61 emoji rows and "Cafe" +
U+0301 (5); assert count, color class and Save state.
7. **Too long** - the 62-character value; assert "62 / 60", the error with circle-alert, "Unsaved
changes", Save disabled, Enter sends nothing; shorten and assert the error clears.
8. **Dirty rules, Examples-driven** - each row.
9. **Enter saves** - assert one PATCH with body `{"display_name":"Marta Alves"}`.
10. **Saving** - unresolved PATCH with a padded value; assert the trimmed body, disabled and dimmed
field, "Saving…", and that Enter and a second click send nothing.
11. **Saved** - resolve 200; assert session.displayName, chip "Marta Alves" with "MA", field value,
"11 / 60", footer, Save disabled, the toast arguments, and no leave dialog after navigating.
12. **Server value wins** - resolve with a trimmed display_name; assert all consumers use it.
13. **Clearing** - saved "Marta Alves", empty the field, save; assert body `{"display_name":""}`,
chip "marta", footer, and the "Display name removed" toast.
14. **Save failed, Examples-driven** - `ApiError(500)` and `NetworkError`; assert the alert copy,
the kept value, the "Unsaved changes" footer, Save enabled, the session unchanged and no toast;
save again and assert the alert clears during the request and PATCH is sent again.
15. **400, Examples-driven** - `ApiError(400)` with the display-name text gives the field error;
with another body gives the Save failed alert.
16. **Leave guard** - dirty draft, navigate to /users; assert ConfirmDialog props (title, text,
"Keep editing" as confirm and "Discard changes" as cancel, neutral tone); Keep editing stays;
Discard leaves and leaves the session name unchanged. Also assert: clean draft leaves with no
dialog; a 401 during PATCH redirects with no dialog (authRedirectPending); Log out while dirty
shows the dialog.
17. **Menu, desktop** - click the chip; assert aria-haspopup, aria-expanded, title "Account", the
sand class, the header name and email, the item order (Profile, System, Light, Dark, Log out)
with the two separators, and Profile not highlighted.
18. **Menu, mobile** - below lg, open the drawer and activate the account row; assert the drawer
closed, the sheet with its grab handle, the 36 px avatar header and the same item order.
19. **Closing** - Escape and an outside click; assert closed, aria-expanded false, focus on the
chip; ArrowDown and ArrowUp move between items.
20. **Menu items** - Profile navigates and closes; on /profile it is highlighted, closes with no
navigation; Log out calls POST /v1/auth/logout, clears the session and goes to /login.
21. **Chip fill** - sand on /profile with the menu closed; not sand on /home.
22. **Theme** - activating "Dark" sets `useColorMode().preference`, closes the menu, and reopening
shows "Dark" checked.
23. **Initials, Examples-driven** - each row, including "Élodie Martin" giving "ÉM", a single
word giving two letters, and username "m.alves" giving "M." (first two characters).
24. **Collapsed rail** - avatar only, aria-label "Marta Alves — account menu", same menu.
25. **Home greeting, Examples-driven** - no display name gives "marta"; "Marta Alves" gives
"Marta Alves"; then save on /profile and navigate to /home and assert the new name. Do not
assert the greeting word or the hour.
26. **Reset: sending** - unresolved POST; assert body `{"email":"marta@renew.example"}`, "Sending…",
disabled, no note.
27. **Reset: sent and Send again** - 200 gives "Send again" and the mail-check note with the exact
copy; "Send again" hides the note during the request, sends the same body, and on 200 shows it
again; remounting resets to idle.
28. **Reset: 429, Examples-driven** - reject with `ApiError(429, <retryAfter>)`; assert the warning
note copy, disabled button reading "Send reset link"; advance fake timers by the table's
seconds minus 1 and assert still disabled, then by 1 and assert enabled with the note gone.
Rows: 42, 300, undefined (300), 0 (300).
29. **Reset: failed, Examples-driven** - `ApiError(500)` and `NetworkError`; assert the error note
copy, enabled button, and that a retry resolving 200 gives the sent note.
30. **Users list** - mount UsersPage with session display name "Marta Alves" and the users_screen
Default items where marta and ines carry display names; assert the marta row shows
"Marta Alves" then "marta" and the "you" tag, ines shows "Inès Duarte" then "ines", jonas
shows alone, and the chip reads "Marta Alves".
## Visual (task visual:check -- profile, and -- home; desktop and mobile, light and dark)
Reference: `docs/design/screens/profile.md`, board group "Profile" in `docs/design/source/RenewCMS
Screens Back-office.dc.html`, exact values in `docs/design/source/AppFrame.dc.html`. Add each
state's setup to `web/visual/states.ts` with the board's own sample data. The gate is hard: every
capture must be within 10 %. Base session mock: GET /v1/auth/me 200 with id 1, username
"marta", email "marta@renew.example", is_verified true, display_name "" (state-specific
overrides below).
- [ ] Account menu: /home with `now: boardNow` and the home Default fixtures, session as above
(id 1, "marta", "marta@renew.example", display_name ""), menu opened by clicking the chip
(on mobile: open the drawer, then the account row). Name "marta", initials "MA", email
"marta@renew.example". The board now draws System, Light and Dark, with Light checked in
its sample. The `exception` block is removed from web/visual/states.ts, so the default 10 %
gate applies. `task visual:check -- profile "Account menu"` must PASS on all four captures
(desktop and mobile, light and dark). If the only remaining diff is the board's Profile
highlight on Home (the app highlights Profile only on /profile), do NOT re-add an exception
and do NOT highlight Profile on Home. Stop and ask the user.
- [ ] Manual: item heights measured 32 px (desktop popover) and 48 px (mobile sheet); arrow keys move
between items; Escape and outside click close; choosing a theme keeps the menu open.
- [ ] Loading: /profile with GET /v1/auth/me hanging; three skeleton cards, 16 px apart.
- [ ] Default, no display name: empty field, "0 / 60", the username footer, Save at 40 %.
- [ ] Editing: type "Marta Alves"; field focused; "11 / 60"; "Unsaved changes"; Save enabled.
- [ ] Too long: type "Marta Alves, Head of Renovations, Brussels and Wallonia office"; "62 / 60"
red; error "Use 60 characters or fewer."; Save at 40 %.
- [ ] Saving: type "Marta Alves", press Save with PATCH hanging; disabled dimmed field, "Saving…".
- [ ] Saved: PATCH 200 with display_name "Marta Alves"; chip "Marta Alves" "MA"; toast "Display name
saved" / "You now appear as Marta Alves across RenewCMS and in emails."
- [ ] Save failed: PATCH 500 after typing "Marta Alves"; the error alert in the card; "Unsaved changes".
- [ ] Reset: sending: session display_name "Marta Alves"; POST /v1/auth/password-reset hanging;
"Sending…".
- [ ] Reset: sent: same session; POST 200; "Send again" and the "Reset link sent" note.
- [ ] Reset: too many requests (429): POST 429 with header `Retry-After: 300`; disabled button at
40 % and the "Too many requests" note.
- [ ] Reset: failed: POST 500; the "Couldn't send the link" note.
- [ ] Display names in the Users list: /users, session display_name "Marta Alves"; GET /v1/user/
with the five board rows (sam.oliveira@renew.example pending 27 Sep 2026, p.huber@gmail.com
expired 3 Aug 2026, ines "Inès Duarte" 18 Jun 2026, jonas 2 Apr 2026, marta "Marta Alves"
12 Mar 2026 with "you"). Clock-sensitive rendering is skipped per repo rules.
- [ ] `task visual:check -- home` still PASSes for Default, Loading, Empty and Error. The session
fixture has display_name "", so the greeting name is "marta". Extend the fixture with
`email` and `display_name` (it must tolerate them being absent).
- [ ] Layout from the board frame, not components.md: content column 720 px max; cards separated by
20 px; the footer strip has the status text left and Save right (32 px high on desktop, 44 px
on mobile); the account inputs are two columns on desktop and stacked on mobile; the
password input and button are a row (button aligned to the bottom edge) on desktop and
stacked, full width, on mobile.
- [ ] Not covered by screenshots: keyboard and focus order (Tab reaches the field, Save, "Send
reset link"; Escape returns focus to the chip; focus returns to the field after a failed
save), hover and active states, motion (popover and sheet, skeleton pulse 1.6 s, both
stopped under reduced motion).
- [ ] Not drawn, no visual state: the leave-page dialog, the "Display name removed" toast, the
400 variants. Vitest only (cases 13, 15, 16).
## Manual verification
- **Real round trips against `task dev`.** Vitest only mocks `useApi`. Save a display name and
confirm the chip, menu, Home greeting and Users list show it after a reload; clear it and
confirm the username returns; confirm login still works only with the username.
- **Unicode limit.** Paste 60 and 61 emoji and a combining-mark name; the client counter and
the API (`utf8.RuneCountInString`) agree at the boundary.
- **Real reset email** (needs real or sandboxed SMTP, `SMTP_HOST` and related vars). "Send reset
link" delivers a mail to the account's own address, the link ends on the reset-confirm screen,
and a completed reset invalidates the current session (`token_version`). "Send again"
invalidates the older link.
- **Real 429.** Exceed `AUTH_RATE_LIMIT_MAX_ATTEMPTS` within `AUTH_RATE_LIMIT_WINDOW_SECONDS` and
confirm the button re-enables when the real Retry-After elapses. The limiter budget is shared
with login.
- **API quirk, out of scope.** POST /v1/auth/password-reset returns 200 even if the mail cannot be
sent (the handler discards the use-case error), so with SMTP down the page still shows "Reset
link sent". Reported, not fixed here.
- **Mobile sheet and toasts on a real device.** The drawer-to-sheet handoff, scrim, safe-area
padding, and the toast placement (bottom center on mobile).
- **Theme persistence.** The choice made from the menu survives a reload.