7.2 KiB
7.2 KiB
QA: Log in screen
Automated (Vitest + Vue Test Utils, task test:web)
Mock useApi (GET /v1/auth/registration, POST /v1/auth/login, GET /v1/auth/me) per case; no
real backend, no httptest server. Mount LoginPage inside AuthShell with a test router whose current
route starts at the path each case needs.
- Default state — mock
GET /v1/auth/registrationresolving{ open: false }; mount at /login; assert heading "Log in", description "Welcome back. Log in to manage your articles.", Username placeholder "Your username" with no value, Password with no value, submit button "Log in", and no "Create the owner account" link. - Fresh install state — mock
GET /v1/auth/registrationresolving{ open: true }; mount at /login; assert the same heading/description/fields as case 1, plus a "Create the owner account" link under "First time here?". - Validation errors state — mount at /login; set Username to "ma", leave Password empty,
submit; assert the mocked
POST /v1/auth/loginwas never called; assert Username shows "Use at least 3 characters." and Password shows "Enter your password." - Submitting state — set Username "marta", Password "limewash2026"; mock
POST /v1/auth/loginwith an unresolved promise; submit; assert Username and Password are disabled and the submit button reads "Logging in…" with a loading indicator. - Wrong credentials state — set Username "marta", Password "wrong-password"; mock
POST /v1/auth/loginresolving 403; submit; assert an error alert with title "That username and password don't match" and description "Check both and try again. Passwords are case-sensitive."; assert Username still reads "marta"; assert Password is empty and has focus; assert the submit button reads "Log in" and is no longer loading. - Too many attempts state — set Username "marta", Password "limewash"; mock
POST /v1/auth/loginresolving 429 with aRetry-After: 42header; submit; assert an info alert with title "Too many attempts" and description "For your security, wait a moment before trying again."; assert Username still reads "marta" and Password still reads "limewash"; assert the submit button reads "Try again in 0:42" and is disabled; assert both fields are disabled; advance fake timers by 42 seconds; assert the button re-enables and reads "Log in" and both fields are editable again. - Session expired state — mount at "/login?redirect=/article/12/edit&reason=expired"; assert an info alert with title "Your session expired" and description "Log in again to pick up where you left off. Unsaved changes are kept on this device."; assert the Password field has focus; assert the submit button reads "Log in".
- Password changed state — mount at "/login?reason=password-changed"; assert a success alert with title "Password changed" and description "Log in with your new password."; assert Username shows placeholder "Your username" with no value.
- Server or network error state — three sub-cases, one per Examples row: (a) a 500 response
with Password "limewash2026"; (b) a rejected fetch (network error) with Password "limewash2026";
(c) a 400 response with Password "short1" (6 characters — passes client validation since it's
non-empty and 3+ characters, but is under the server's 8-character minimum, which the client does
not check, so the request reaches the server and comes back 400). In each sub-case, set Username
"marta" and the row's Password, submit, and assert an error alert with title "Couldn't reach
RenewCMS" and description "Check your connection and try again."; assert Username still reads
"marta" and Password still reads the row's value; assert the submit button reads "Log in" and is
no longer loading. Sub-case (c) also asserts the mocked
POST /v1/auth/loginwas actually called (proving client validation let it through), unlike scenario 3's validation-errors case. - Successful log in redirects correctly — one test per Examples row: mock
POST /v1/auth/loginresolving 200, thenGET /v1/auth/meresolving 200 with the row'sis_verified; mount at the row's path; submit valid credentials; assert the router's current path equals the row's destination.path verified destination /login true /home /login false /register/pending /login?redirect=/article/12/edit true /article/12/edit
Visual (manual verification — desktop and mobile, light and dark)
Reference: docs/design/source/RenewCMS Screens Auth.dc.html, group "Log in".
- Default state, desktop and mobile, light and dark: heading, description, Username, Password with show/hide toggle, "Forgot password?" link, "Log in" button, matches the board exactly.
- Fresh install state: "First time here? Create the owner account" footer renders below the form, desktop and mobile, light and dark.
- Validation errors state: both field errors render inline under their fields with the error styling from components.md, desktop and mobile, light and dark.
- Submitting state: disabled field styling and the loading spinner in the submit button, desktop and mobile, light and dark.
- Wrong credentials state: error alert (circle-x icon,
role="alert") above the heading, cleared/focused Password field, desktop and mobile, light and dark. - Too many attempts state: info alert (timer icon), disabled fields and a disabled button with the live "Try again in m:ss" countdown text, desktop and mobile, light and dark.
- Session expired state: info alert (clock icon, no close button), focused Password field, desktop and mobile, light and dark.
- Password changed state: success alert (circle-check icon, no close button, as the board draws it), desktop and mobile, light and dark.
- Server or network error state: error alert (circle-x icon, no close button), fields retain their values, desktop and mobile, light and dark.
- Keyboard/focus: Tab order is Username → Password → show/hide toggle → "Forgot password?" →
submit; the alert (when present) is announced via
role="status"(info/success) orrole="alert"(error) without re-announcing on re-render; Password receives programmatic focus exactly in the Wrong credentials and Session expired states, and nowhere else.
Manual verification
- Real GET /v1/auth/registration and POST /v1/auth/login against a running server — Vitest only
mocks
useApi; confirm the Default/Fresh install split and a real 403/429/200 round-trip againsttask devwith a live SQLite file, since this slice's automated tests never exercise the actual Go handlers, the rate limiter, or the realjwtcookie. - 429 countdown drift — confirm
useRetryAfter's live countdown does not drift or double-fire the re-enable against a real elapsed 42-second wait, since Vitest fake timers only prove the logic, not real wall-clock behavior.