.agents/skills/playwright-best-practices/advanced/authentication-flows.md
When to use: Testing email verification, password reset, session timeout/expiration, or remember-me functionality. For basic auth setup (storage state, OAuth mocking, MFA, role-based access), see authentication.md.
Intercept API responses to capture verification tokens for testing:
test('completes registration with email verification', async ({page}) => {
let capturedToken = ''
await page.route('**/api/auth/register', async (route) => {
const response = await route.fetch()
const body = await response.json()
capturedToken = body.verificationToken
await route.fulfill({response})
})
await page.goto('/register')
await page.getByLabel('Name').fill('New User')
await page.getByLabel('Email').fill('[email protected]')
await page.getByLabel('Password', {exact: true}).fill('SecurePass!')
await page.getByLabel('Confirm password').fill('SecurePass!')
await page.getByRole('button', {name: 'Create account'}).click()
await expect(page.getByText('Check your inbox')).toBeVisible()
expect(capturedToken).toBeTruthy()
await page.goto(`/verify?token=${capturedToken}`)
await expect(page.getByText('Email confirmed')).toBeVisible()
})
test('verifies email with mocked endpoints', async ({page}) => {
const mockToken = 'test-verification-abc123'
await page.route('**/api/auth/register', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({message: 'Verification sent', verificationToken: mockToken}),
})
})
await page.route(`**/api/auth/verify?token=${mockToken}`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({verified: true}),
})
})
await page.goto('/register')
await page.getByLabel('Email').fill('[email protected]')
await page.getByLabel('Password', {exact: true}).fill('Password123!')
await page.getByRole('button', {name: 'Sign up'}).click()
await expect(page.getByText('Check your inbox')).toBeVisible()
await page.goto(`/verify?token=${mockToken}`)
await expect(page.getByText('Email confirmed')).toBeVisible()
})
test('resets password through email link', async ({page}) => {
let resetToken = ''
await page.route('**/api/auth/forgot-password', async (route) => {
const response = await route.fetch()
const body = await response.json()
resetToken = body.resetToken
await route.fulfill({response})
})
await page.goto('/forgot-password')
await page.getByLabel('Email').fill('[email protected]')
await page.getByRole('button', {name: 'Send link'}).click()
await expect(page.getByText('Reset email sent')).toBeVisible()
expect(resetToken).toBeTruthy()
await page.goto(`/reset-password?token=${resetToken}`)
await page.getByLabel('New password', {exact: true}).fill('NewPassword456!')
await page.getByLabel('Confirm password').fill('NewPassword456!')
await page.getByRole('button', {name: 'Update password'}).click()
await expect(page.getByText('Password updated')).toBeVisible()
})
test('shows error for expired reset token', async ({page}) => {
await page.goto('/reset-password?token=expired-token')
await page.getByLabel('New password', {exact: true}).fill('NewPass!')
await page.getByLabel('Confirm password').fill('NewPass!')
await page.getByRole('button', {name: 'Update password'}).click()
await expect(page.getByRole('alert')).toContainText(/expired|invalid/i)
})
test('enforces password requirements on reset', async ({page}) => {
await page.goto('/reset-password?token=valid-token')
await page.getByLabel('New password', {exact: true}).fill('weak')
await page.getByLabel('Confirm password').fill('weak')
await page.getByRole('button', {name: 'Update password'}).click()
await expect(page.getByText(/at least 8 characters/i)).toBeVisible()
})
test('redirects to signin after session expires', async ({page, context}) => {
await page.goto('/signin')
await page.getByLabel('Email').fill('[email protected]')
await page.getByLabel('Password').fill('Password!')
await page.getByRole('button', {name: 'Sign in'}).click()
await expect(page).toHaveURL('/home')
const cookies = await context.cookies()
const sessionCookie = cookies.find((c) => c.name.includes('session'))
if (sessionCookie) {
await context.clearCookies({name: sessionCookie.name})
}
await page.goto('/profile')
await expect(page).toHaveURL(/\/signin/)
await expect(page.getByText(/session.*expired|sign in again/i)).toBeVisible()
})
test('shows warning before session expires', async ({page}) => {
await page.route('**/api/auth/session', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({valid: true, expiresIn: 60}),
})
})
await page.goto('/home')
await expect(page.getByText(/session.*expir/i)).toBeVisible({timeout: 10000})
await expect(page.getByRole('button', {name: /extend|stay signed in/i})).toBeVisible()
})
test('extends session when user clicks extend', async ({page}) => {
let sessionExtended = false
await page.route('**/api/auth/session', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({valid: true, expiresIn: 60}),
})
})
await page.route('**/api/auth/refresh', async (route) => {
sessionExtended = true
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({valid: true, expiresIn: 3600}),
})
})
await page.goto('/home')
await expect(page.getByRole('button', {name: /extend|stay signed in/i})).toBeVisible({
timeout: 10000,
})
await page.getByRole('button', {name: /extend|stay signed in/i}).click()
expect(sessionExtended).toBe(true)
await expect(page.getByText(/session.*expir/i)).not.toBeVisible()
})
test('persists session with remember me enabled', async ({browser}) => {
const ctx1 = await browser.newContext()
const page1 = await ctx1.newPage()
await page1.goto('/signin')
await page1.getByLabel('Email').fill('[email protected]')
await page1.getByLabel('Password').fill('Password!')
await page1.getByLabel('Keep me signed in').check()
await page1.getByRole('button', {name: 'Sign in'}).click()
await expect(page1).toHaveURL('/home')
const state = await ctx1.storageState()
await ctx1.close()
const ctx2 = await browser.newContext({storageState: state})
const page2 = await ctx2.newPage()
await page2.goto('/home')
await expect(page2).toHaveURL('/home')
await expect(page2.getByText('Welcome')).toBeVisible()
await ctx2.close()
})
test('session-only login does not persist across browser restarts', async ({browser}) => {
const ctx1 = await browser.newContext()
const page1 = await ctx1.newPage()
await page1.goto('/signin')
await page1.getByLabel('Email').fill('[email protected]')
await page1.getByLabel('Password').fill('Password!')
// Leave "Remember me" unchecked
await expect(page1.getByLabel('Keep me signed in')).not.toBeChecked()
await page1.getByRole('button', {name: 'Sign in'}).click()
await expect(page1).toHaveURL('/home')
// Only keep persistent cookies (filter out session cookies)
const cookies = await ctx1.cookies()
await ctx1.close()
const persistentCookies = cookies.filter((c) => c.expires > 0)
const ctx2 = await browser.newContext()
await ctx2.addCookies(persistentCookies)
const page2 = await ctx2.newPage()
await page2.goto('/home')
// Should redirect to login since session was not persisted
await expect(page2).toHaveURL(/\/signin/)
await ctx2.close()
})
test.use({storageState: '.auth/user.json'})
test('logs out and clears session', async ({page, context}) => {
await page.goto('/home')
await page.getByRole('button', {name: /account|menu/i}).click()
await page.getByRole('menuitem', {name: 'Sign out'}).click()
await expect(page).toHaveURL('/signin')
const cookies = await context.cookies()
const sessionCookies = cookies.filter(
(c) => c.name.includes('session') || c.name.includes('token'),
)
expect(sessionCookies).toHaveLength(0)
await page.goto('/home')
await expect(page).toHaveURL(/\/signin/)
})
test('logs out from all devices', async ({page}) => {
let logoutAllCalled = false
await page.route('**/api/auth/logout-all', async (route) => {
logoutAllCalled = true
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({message: 'Logged out everywhere'}),
})
})
await page.goto('/settings/security')
await page.getByRole('button', {name: 'Sign out everywhere'}).click()
await page.getByRole('dialog').getByRole('button', {name: 'Confirm'}).click()
expect(logoutAllCalled).toBe(true)
await expect(page).toHaveURL(/\/signin/)
})