# QA: Pending registration screen ## Automated (Vitest + Vue Test Utils, `task test:web`) Mock `useApi` (`DELETE /v1/auth/register`, `POST /v1/auth/logout`) and `useToast`/`useOverlay` per case; no real backend, no httptest server. Mount PendingPage inside AuthShell with a test router and a test Pinia session store. Seed/clear `sessionStorage["renewcms:pendingEmail"]` per case. 1. **Default state** — set `sessionStorage["renewcms:pendingEmail"] = "marta@renew.example"`; mount at /register/pending; assert a ResultPanel with tone "success", icon "i-lucide-mail-check", title "Check your email", text "We sent a verification link to marta@renew.example. It expires in 2 hours.", the note paragraph rendered via the default slot, and two secondary actions "Log out" and "Cancel registration". 2. **Email unknown state** — clear `sessionStorage["renewcms:pendingEmail"]`; mount at /register/pending; assert the same ResultPanel shape with text "We sent a verification link to the email you registered with. It expires in 2 hours." and the same note and actions. 3. **Logging out** — mount with an unresolved `POST /v1/auth/logout` mock; click "Log out"; assert both actions are disabled; resolve the mock 200; assert the session store's `clear()` was called and the router's current path is /login. 4. **Confirm cancel opens the dialog** — click "Cancel registration"; assert a ConfirmDialog renders with title "Cancel your registration?", description "Your account will be deleted. You can register again with the same or a different email.", buttons "Keep my account" and "Cancel registration" (tone danger), `role="alertdialog"`, and initial focus on "Keep my account". 5. **Cancelling state** — with the dialog open, mock `DELETE /v1/auth/register` with an unresolved promise, click "Cancel registration" inside the dialog; assert both dialog buttons are disabled, the confirm button reads "Cancelling…", `aria-busy="true"` on the dialog, and the mocked call was made with `{ onForbidden: 'passthrough' }`. 6. **Keep my account** — with the dialog open, click "Keep my account"; assert the dialog closes, `DELETE /v1/auth/register` was never called, and the underlying ResultPanel is unchanged. 7. **Cancelled state** — confirm the dialog; mock `DELETE /v1/auth/register` resolving 200; assert the dialog closes, the router's current path is /register, and a toast was raised with color "success", icon "i-lucide-circle-check", title "Registration cancelled", description "You can create the owner account again." 8. **Already verified state** — seed the session store with `{ verified: false }`, `status:'ready'`; confirm the dialog; mock `DELETE /v1/auth/register` resolving 403; mock the session store's `retry()` to resolve and set `session.session = { verified: true }`; assert `retry()` was awaited before the navigation; assert the router's current path is /home; assert a toast was raised with color "info", icon "i-lucide-info", title "Your account is already verified", description "There's nothing to cancel."; assert no ResultPanel assertions target this transition (Pending unmounts as the route changes). 9. **Cancel failed state** — two sub-cases: (a) a 500 response, (b) a rejected fetch. In each, confirm the dialog, resolve/reject the mock accordingly, and assert the dialog stays open, an inline failure indicator is shown inside it (not a toast — assert no toast was raised), and both dialog buttons are re-enabled. 10. **`useApi` onForbidden option (unit-level, alongside the existing suite in `api_client_global_error_handling`'s spec file)** — seed `session = { verified: false }`; mock fetch resolving 403; call `useApi().request('/v1/auth/register', { method: 'DELETE', onForbidden: 'passthrough' })`; assert the promise rejects with `ApiError` status 403; assert the router's current route did not change (no redirect to /register/pending); assert the session store is unchanged. This regresses nothing in the existing onForbidden-absent (default 'redirect') scenarios already covered by `features/api_client_global_error_handling.feature`. ## Visual (manual verification — desktop and mobile, light and dark) Reference: `docs/design/source/RenewCMS Screens Auth.dc.html`, group "Pending". Run `task visual:check -- pending` for each state and compare against the board frame. - [ ] Default state: icon tile, title, text with the interpolated email, note paragraph, "Log out" and "Cancel registration" secondary buttons — matches the board exactly. - [ ] Email unknown state: same layout with the generic text. - [ ] Confirm cancel state: ConfirmDialog layout, button order (Keep my account left, Cancel registration right, danger color), matches components.md's button-order rule. - [ ] Cancelling state: disabled buttons, "Cancelling…" label, spinner/aria-busy styling. - [ ] Cancelled state (transient, capture via visual test hook or a frozen mock): toast appearance over the /register board. - [ ] Already verified state: toast appearance over the /home board (per the design board's illustrative use of Verify's Success ResultPanel as the backdrop). - [ ] Keyboard/focus: focus starts on "Keep my account" in the dialog and Enter does not confirm; while cancelling, focus cannot leave the dialog; on the ResultPanel states, focus lands on the title on mount; toasts are announced without stealing focus. ## Manual verification - **Real DELETE /v1/auth/register and POST /v1/auth/logout against a running server** — Vitest only mocks `useApi`; confirm the real 200/403 round-trips against `task dev` with a live SQLite file. The endpoint's own status/body/cookie contract is already covered by `features/cancel_pending_registration.qa.md`; this slice only re-verifies the screen consumes it correctly. - **Router-guard staleness fix in a real browser** — confirm that after the "Already verified" 403, a hard reload of /home does not bounce back to /register/pending, since Vitest's fake session store can't fully reproduce the router's `afterEach` re-check timing.