feat: password reset full design and flow

This commit is contained in:
fsylvain committed 2026-09-28 05:06:16 +02:00
1 parent 938263cd6a
commit cb05b38f36
9 files changed
+1471 -4

No files matched your search

+109
View File
@@ -392,6 +392,51 @@ async function chooseArticleRowMenuItem(page: Page, label: string) {
await page.getByRole('menuitem', { name: label, exact: true }).click()
}
async function fillResetRequestEmail(page: Page, email: string) {
await page.getByPlaceholder('you@example.com').fill(email)
}
function submitResetRequest(email: string) {
return async (page: Page) => {
await fillResetRequestEmail(page, email)
await page.getByRole('button', { name: 'Send reset link', exact: true }).click()
await page.mouse.move(0, 0)
}
}
function resetRequestWith(response: ApiMock): Omit<VisualState, 'steps'> {
return {
path: '/register/reset/request',
api: {
'POST /v1/auth/password-reset': response,
},
}
}
const resetConfirmPath = '/register/reset/validate?c=abc123&email=marta%40renew.example'
async function fillResetConfirm(page: Page, password: string, confirmPassword: string) {
await page.locator('#reset-new-password').fill(password)
await page.locator('#reset-confirm-password').fill(confirmPassword)
}
function submitResetConfirm(password: string, confirmPassword: string) {
return async (page: Page) => {
await fillResetConfirm(page, password, confirmPassword)
await page.getByRole('button', { name: 'Change password', exact: true }).click()
await page.mouse.move(0, 0)
}
}
function resetConfirmWith(response: ApiMock): Omit<VisualState, 'steps'> {
return {
path: resetConfirmPath,
api: {
'POST /v1/auth/password-reset/confirm': response,
},
}
}
const integrationOrigin = 'https://cms.renew.example'
async function clickBaseUrlCopy(page: Page) {
@@ -1022,4 +1067,68 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
path: '/register/validate',
},
},
'forgot-password': {
Default: { path: '/register/reset/request' },
'Validation error': {
path: '/register/reset/request',
steps: submitResetRequest('marta@renew'),
},
Submitting: {
...resetRequestWith({ status: 200, hang: true }),
steps: submitResetRequest('marta@renew.example'),
},
Sent: {
...resetRequestWith({ status: 200, body: { message: 'ok' } }),
steps: submitResetRequest('marta@renew.example'),
},
'Too many attempts (429)': {
...resetRequestWith({ status: 429, headers: { 'Retry-After': '58' } }),
steps: submitResetRequest('marta@renew.example'),
},
'Server error': {
...resetRequestWith({ status: 500 }),
steps: submitResetRequest('marta@renew.example'),
},
},
'reset-password': {
Default: { path: resetConfirmPath },
'Too short': {
path: resetConfirmPath,
steps: submitResetConfirm('lime', 'lime'),
},
Mismatch: {
path: resetConfirmPath,
steps: async (page) => {
await fillResetConfirm(page, 'limewash2026', 'limewash2025')
await page.locator('#reset-confirm-password').blur()
},
},
Submitting: {
...resetConfirmWith({ status: 200, hang: true }),
steps: submitResetConfirm('limewash2026', 'limewash2026'),
},
Success: {
path: resetConfirmPath,
api: {
'POST /v1/auth/password-reset/confirm': { status: 200, body: { message: 'ok' } },
'GET /v1/auth/registration': registrationClosed,
},
steps: async (page) => {
await submitResetConfirm('limewash2026', 'limewash2026')(page)
await page.waitForURL('**/login**')
await page.getByText('Password changed').waitFor()
},
},
'Invalid or expired (403)': {
...resetConfirmWith({ status: 403 }),
steps: submitResetConfirm('limewash2026', 'limewash2026'),
},
'Missing parameters': {
path: '/register/reset/validate',
},
'Server error': {
...resetConfirmWith({ status: 500 }),
steps: submitResetConfirm('limewash2026', 'limewash2026'),
},
},
}