165 lines
12 KiB
Markdown
165 lines
12 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,
|
|
menu opened by clicking the chip (on mobile: open the drawer, then the account row). Name
|
|
"marta", initials "MA", email "marta@renew.example". The theme items kept by user decision
|
|
are not drawn on the board, so the mobile capture is expected to exceed 10 %. The user has
|
|
approved a per-state `exception` for it, to be added with `approvedBy` only after the
|
|
real difference is measured and reported to the user with its cause.
|
|
- [ ] 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.
|