154 lines
10 KiB
Gherkin
154 lines
10 KiB
Gherkin
Feature: Pending registration screen
|
|
|
|
As a logged-in user who has not yet verified their account
|
|
I want /register/pending to tell me where the verification link went, let me log out or cancel
|
|
my registration, and route me to the right destination no matter which of those calls
|
|
succeeds or fails
|
|
So that I always know what to do next while waiting for the emailed link, and a device that
|
|
verified elsewhere or a server error never strands me on the wrong screen
|
|
|
|
Background:
|
|
Given /register/pending is a "session" route (meta.auth "session"), rendered as PendingPage
|
|
inside AuthShell, per docs/design/routes.md; "session" only requires a logged-in account, so
|
|
both a verified and an unverified account can land on it without being redirected away by
|
|
the router guard
|
|
And PendingPage only calls DELETE /v1/auth/register and POST /v1/auth/logout
|
|
And RegisterView.vue, on a 200 from POST /v1/auth/register, writes the submitted email to
|
|
sessionStorage under the key "renewcms:pendingEmail" before navigating to /register/pending;
|
|
this is the only change this slice makes to RegisterView.vue
|
|
And on mount, PendingPage reads "renewcms:pendingEmail" from sessionStorage once: if present,
|
|
it renders the Default state with that email interpolated into the ResultPanel text; if
|
|
absent (for example, the user arrived by logging in as an unverified account rather than by
|
|
just registering), it renders the Email unknown state with the generic text
|
|
And in both cases the ResultPanel has tone "success", icon "i-lucide-mail-check", title "Check
|
|
your email", and two secondary actions, "Log out" and "Cancel registration" (color
|
|
"neutral", variant "outline"); the note line ("Opened it on another device? That works too;
|
|
you'll log in there first. Can't find it? Check your spam folder. If the link has expired,
|
|
cancel your registration and start again.") is rendered through ResultPanel's default slot
|
|
as a second paragraph below the description, per components.md's "Slot default for extras"
|
|
And "Log out" calls POST /v1/auth/logout, which per loginHandler.Logout always responds 200 and
|
|
clears the jwt cookie; on its response, PendingPage calls session.clear() and navigates to
|
|
/login, disabling both actions while the call is in flight
|
|
And "Cancel registration" opens ConfirmDialog (built by this slice; components.md documents it
|
|
but no file exists yet) via useOverlay().create(ConfirmDialog).open(), with title "Cancel
|
|
your registration?", description "Your account will be deleted. You can register again with
|
|
the same or a different email.", confirmLabel "Cancel registration", cancelLabel "Keep my
|
|
account" (components.md's "Other uses" line pairs title/description/confirmLabel/cancelLabel
|
|
in that order, so ConfirmDialog exposes a cancelLabel prop alongside confirmLabel even though
|
|
the compact props line only names confirmLabel), pendingLabel "Cancelling…", and tone
|
|
"danger"; its action prop performs the DELETE /v1/auth/register call
|
|
And while the dialog's action is in flight (Cancelling), both buttons are disabled, the confirm
|
|
button reads "Cancelling…", aria-busy is set, and the dialog cannot be dismissed, per
|
|
ConfirmDialog's general contract in components.md
|
|
And PendingPage's DELETE /v1/auth/register call passes { onForbidden: 'passthrough' } (a new
|
|
RequestOptions field on useApi, added by this slice, mirroring the existing onUnauthorized
|
|
field: type 'redirect' | 'passthrough', default 'redirect'; when 'passthrough' is given, the
|
|
403 branch in useApi's request() skips the "if session.session?.verified === false, push to
|
|
/register/pending" step entirely and always throws ApiError(403)) so PendingPage's own 403
|
|
handling below runs instead of the global handler bouncing it back to /register/pending
|
|
And on a 200 from DELETE /v1/auth/register, the action resolves, the dialog closes, the app
|
|
navigates to /register, and a toast (color "success", icon "i-lucide-circle-check", title
|
|
"Registration cancelled", description "You can create the owner account again.") is shown
|
|
And on a 403 from DELETE /v1/auth/register (user.ErrAlreadyVerified, since the account was
|
|
verified on another device while this tab still holds a stale unverified session), the
|
|
action first awaits session.retry() so the session store's cached verified flag is refreshed
|
|
to true — without this, the router guard's stale local state would immediately bounce a
|
|
navigation to /home straight back to /register/pending — then the dialog closes,
|
|
PendingPage replaces its panel with a ResultPanel (tone "success", icon
|
|
"i-lucide-shield-check", title "You're verified", text "Taking you to your dashboard…", no
|
|
actions) and shows a toast (color "info", icon "i-lucide-info", title "Your account is already
|
|
verified", description "There's nothing to cancel.") over it, exactly as the design board
|
|
draws this state, then navigates to /home after the same 800 ms Verify's Success uses
|
|
And on a 500 response or a request that never reaches the server, the action rejects, the
|
|
dialog stays open, and it shows the failure inline (per ConfirmDialog's general contract:
|
|
"the action runs inside the dialog, so errors show there instead of a toast"), re-enabling
|
|
both buttons so the user can retry or choose "Keep my account"
|
|
And a 401 from either call is handled entirely by useApi's existing global 401 handling
|
|
(redirect to /login?redirect=...&reason=expired); PendingPage adds no special case for it
|
|
|
|
Scenario: Default — the email is known from a just-completed registration
|
|
Given sessionStorage holds "renewcms:pendingEmail" set to "marta@renew.example"
|
|
When the user navigates to /register/pending
|
|
Then a ResultPanel is shown with tone "success" and icon "i-lucide-mail-check"
|
|
And its title is "Check your email"
|
|
And its text is "We sent a verification link to marta@renew.example. It expires in 2 hours."
|
|
And its note reads "Opened it on another device? That works too; you'll log in there first.
|
|
Can't find it? Check your spam folder. If the link has expired, cancel your registration and
|
|
start again."
|
|
And its actions are "Log out" and "Cancel registration", both secondary
|
|
|
|
Scenario: Email unknown — arrived by logging in as an unverified account
|
|
Given sessionStorage holds no "renewcms:pendingEmail" entry
|
|
When the user navigates to /register/pending
|
|
Then a ResultPanel is shown with tone "success" and icon "i-lucide-mail-check"
|
|
And its title is "Check your email"
|
|
And its text is "We sent a verification link to the email you registered with. It expires in 2
|
|
hours."
|
|
And its note is the same as the Default state's note
|
|
And its actions are "Log out" and "Cancel registration", both secondary
|
|
|
|
Scenario: Logging out
|
|
Given the user is on /register/pending
|
|
When the user activates "Log out"
|
|
Then both actions are disabled while POST /v1/auth/logout is in flight
|
|
When the response resolves 200
|
|
Then the session store is cleared
|
|
And the app navigates to /login
|
|
|
|
Scenario: Confirm cancel — the destructive dialog opens
|
|
Given the user is on /register/pending
|
|
When the user activates "Cancel registration"
|
|
Then a ConfirmDialog opens with title "Cancel your registration?", description "Your account
|
|
will be deleted. You can register again with the same or a different email.", a "Keep my
|
|
account" cancel button, a "Cancel registration" confirm button in tone "danger", role
|
|
"alertdialog", and focus starting on "Keep my account"
|
|
And the underlying ResultPanel is still shown behind it, unchanged
|
|
|
|
Scenario: Cancelling — the DELETE is in flight and the dialog can't be dismissed
|
|
Given the ConfirmDialog from "Confirm cancel" is open
|
|
When the user activates "Cancel registration" inside the dialog
|
|
Then both dialog buttons are disabled, the confirm button reads "Cancelling…", aria-busy is set
|
|
on the dialog, and it cannot be dismissed by Escape or an overlay click
|
|
And DELETE /v1/auth/register is sent with { onForbidden: 'passthrough' }
|
|
|
|
Scenario: Keep my account — the dialog is dismissed without deleting anything
|
|
Given the ConfirmDialog from "Confirm cancel" is open
|
|
When the user activates "Keep my account"
|
|
Then the dialog closes, emitting close(false)
|
|
And DELETE /v1/auth/register is never called
|
|
And the ResultPanel from before the dialog opened is still shown, unchanged
|
|
|
|
Scenario: Cancelled — 200 navigates to /register with a toast
|
|
Given the ConfirmDialog's action is in flight after the user confirmed
|
|
When DELETE /v1/auth/register resolves 200
|
|
Then the dialog closes
|
|
And the app navigates to /register
|
|
And a toast is shown with color "success", icon "i-lucide-circle-check", title "Registration
|
|
cancelled", description "You can create the owner account again."
|
|
|
|
Scenario: Already verified — 403 shows the verified panel and toast, then navigates to /home
|
|
Given the ConfirmDialog's action is in flight after the user confirmed
|
|
And the session store currently holds { verified: false }
|
|
When DELETE /v1/auth/register resolves 403 with the apperr.AlreadyVerifiedErrorMessage body
|
|
Then session.retry() is awaited before any navigation happens
|
|
And once it resolves the session store holds { verified: true }
|
|
And the dialog closes
|
|
And a ResultPanel is shown with tone "success", icon "i-lucide-shield-check", title "You're
|
|
verified", text "Taking you to your dashboard…", and no actions
|
|
And a toast is shown over it with color "info", icon "i-lucide-info", title "Your account is
|
|
already verified", description "There's nothing to cancel."
|
|
And after 800 ms the app navigates to /home
|
|
|
|
Scenario Outline: Cancel failed — the dialog stays open and shows the failure inline
|
|
Given the ConfirmDialog's action is in flight after the user confirmed
|
|
When DELETE /v1/auth/register <failure>
|
|
Then the dialog does not close
|
|
And the failure is shown inline in the dialog, not as a toast
|
|
And both dialog buttons are re-enabled so the user can retry "Cancel registration" or choose
|
|
"Keep my account"
|
|
|
|
Examples:
|
|
| failure |
|
|
| fails with a 500 response |
|
|
| never reaches the server |
|