235 lines
7.0 KiB
TypeScript
235 lines
7.0 KiB
TypeScript
import type { Page } from 'playwright'
|
|
|
|
export interface ApiMock {
|
|
status: number
|
|
body?: unknown
|
|
headers?: Record<string, string>
|
|
hang?: boolean
|
|
}
|
|
|
|
export interface VisualState {
|
|
path: string
|
|
api?: Record<string, ApiMock>
|
|
steps?: (page: Page) => Promise<void>
|
|
}
|
|
|
|
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<VisualState, 'steps'> {
|
|
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<VisualState, 'steps'> {
|
|
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<string, Record<string, VisualState>> = {
|
|
'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',
|
|
},
|
|
},
|
|
}
|