import type { Page } from 'playwright' export interface ApiMock { status: number body?: unknown headers?: Record hang?: boolean } export interface VisualState { path: string api?: Record steps?: (page: Page) => Promise } const registrationClosed: ApiMock = { status: 200, body: { open: false } } const registrationOpen: ApiMock = { status: 200, body: { open: true } } async function fillLogin(page: Page, username: string, password: string) { await page.getByPlaceholder('Your username').fill(username) await page.locator('input[autocomplete="current-password"]').fill(password) } function submitLogin(username: string, password: string) { return async (page: Page) => { await fillLogin(page, username, password) await page.getByRole('button', { name: 'Log in', exact: true }).click() } } function loginWith(response: ApiMock, path = '/login'): Omit { return { path, api: { 'GET /v1/auth/registration': registrationClosed, 'POST /v1/auth/login': response, }, } } async function fillRegister(page: Page, username: string, email: string, password: string) { await page.getByPlaceholder('e.g. marta').fill(username) await page.getByPlaceholder('e.g. marta').blur() await page.getByPlaceholder('you@example.com').fill(email) await page.getByPlaceholder('you@example.com').blur() await page.locator('input[autocomplete="new-password"]').fill(password) await page.locator('input[autocomplete="new-password"]').blur() } function registerWith(response: ApiMock): Omit { return { path: '/register', api: { 'POST /v1/auth/register': response, }, } } function submitRegister(username: string, email: string, password: string) { return async (page: Page) => { await fillRegister(page, username, email, password) await page.getByRole('button', { name: 'Create account', exact: true }).click() } } async function setPendingEmail(page: Page, email: string) { await page.evaluate((value) => sessionStorage.setItem('renewcms:pendingEmail', value), email) await page.reload() await page.waitForLoadState('networkidle') } async function openCancelDialog(page: Page) { await page.getByRole('button', { name: 'Cancel registration', exact: true }).click() } async function confirmCancelDialog(page: Page) { await page .getByRole('alertdialog') .getByRole('button', { name: 'Cancel registration', exact: true }) .click() } export const visualStates: Record> = { 'log-in': { Default: { path: '/login', api: { 'GET /v1/auth/registration': registrationClosed } }, 'Fresh install': { path: '/login', api: { 'GET /v1/auth/registration': registrationOpen } }, 'Validation errors': { ...loginWith({ status: 500 }), steps: submitLogin('ma', ''), }, Submitting: { ...loginWith({ status: 200, hang: true }), steps: submitLogin('marta', 'limewash2026'), }, 'Wrong credentials (403)': { ...loginWith({ status: 403, body: 'forbidden' }), steps: submitLogin('marta', 'limewash2026'), }, 'Too many attempts (429)': { ...loginWith({ status: 429, headers: { 'Retry-After': '42' } }), steps: submitLogin('marta', 'limewash'), }, 'Session expired': { ...loginWith({ status: 500 }, '/login?redirect=/article/12/edit&reason=expired'), steps: async (page) => { await page.getByPlaceholder('Your username').fill('marta') await page.locator('input[autocomplete="current-password"]').focus() }, }, 'Password changed': { path: '/login?reason=password-changed', api: { 'GET /v1/auth/registration': registrationClosed }, }, 'Server or network error': { ...loginWith({ status: 500 }), steps: submitLogin('marta', 'limewash2026'), }, }, register: { Default: { path: '/register' }, 'Validation errors': { path: '/register', steps: async (page) => { await fillRegister(page, 'ma', 'marta@renew', 'lime') }, }, 'Filled, valid': { path: '/register', steps: async (page) => { await fillRegister(page, 'marta', 'marta@renew.example', 'limewash2026') await page.getByRole('button', { name: 'Show password' }).click() }, }, Submitting: { ...registerWith({ status: 200, hang: true }), steps: submitRegister('marta', 'marta@renew.example', 'limewash2026'), }, 'Registration closed (403)': { ...registerWith({ status: 403 }), steps: submitRegister('marta', 'marta@renew.example', 'limewash2026'), }, 'Server error': { ...registerWith({ status: 500 }), steps: submitRegister('marta', 'marta@renew.example', 'limewash2026'), }, }, pending: { Default: { path: '/register/pending', api: { 'GET /v1/auth/me': { status: 200, body: { is_verified: false } } }, steps: async (page) => { await setPendingEmail(page, 'marta@renew.example') }, }, 'Email unknown': { path: '/register/pending', api: { 'GET /v1/auth/me': { status: 200, body: { is_verified: false } } }, }, 'Confirm cancel': { path: '/register/pending', api: { 'GET /v1/auth/me': { status: 200, body: { is_verified: false } } }, steps: async (page) => { await setPendingEmail(page, 'marta@renew.example') await openCancelDialog(page) }, }, Cancelling: { path: '/register/pending', api: { 'GET /v1/auth/me': { status: 200, body: { is_verified: false } }, 'DELETE /v1/auth/register': { status: 200, hang: true }, }, steps: async (page) => { await setPendingEmail(page, 'marta@renew.example') await openCancelDialog(page) await confirmCancelDialog(page) }, }, Cancelled: { path: '/register/pending', api: { 'GET /v1/auth/me': { status: 200, body: { is_verified: false } }, 'DELETE /v1/auth/register': { status: 200, body: { message: 'ok' } }, }, steps: async (page) => { await setPendingEmail(page, 'marta@renew.example') await openCancelDialog(page) await confirmCancelDialog(page) await page.waitForURL('**/register') }, }, 'Already verified (403)': { path: '/register/pending', api: { 'DELETE /v1/auth/register': { status: 403 }, 'GET /v1/auth/me': { status: 200, body: { is_verified: true } }, }, steps: async (page) => { await setPendingEmail(page, 'marta@renew.example') await openCancelDialog(page) await confirmCancelDialog(page) await page.waitForURL('**/home') }, }, }, verify: { Verifying: { path: '/register/validate?c=abc123', api: { 'POST /v1/auth/verify': { status: 200, hang: true } }, }, Success: { path: '/register/validate?c=abc123', api: { 'POST /v1/auth/verify': { status: 200, body: { message: 'ok' } }, 'GET /v1/auth/me': { status: 200, body: { is_verified: true } }, }, }, 'Invalid or expired (403)': { path: '/register/validate?c=abc123', api: { 'POST /v1/auth/verify': { status: 403 } }, }, 'No session (401)': { path: '/register/validate?c=abc123', api: { 'POST /v1/auth/verify': { status: 401 } }, }, 'No session, retrying': { path: '/register/validate?c=abc123', api: { 'POST /v1/auth/verify': { status: 200, hang: true } }, }, 'Missing code': { path: '/register/validate', }, }, }