Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
advanced/authentication-flows.md
1# Complex Authentication Flow Patterns23## Table of Contents451. [Email Verification Flows](#email-verification-flows)62. [Password Reset](#password-reset)73. [Session Timeout](#session-timeout)84. [Remember Me Persistence](#remember-me-persistence)95. [Logout Patterns](#logout-patterns)106. [Tips](#tips)117. [Related](#related)1213> **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](authentication.md).1415---1617## Email Verification Flows1819### Capturing Verification Tokens2021Intercept API responses to capture verification tokens for testing:2223```typescript24test('completes registration with email verification', async ({ page }) => {25let capturedToken = '';2627await page.route('**/api/auth/register', async (route) => {28const response = await route.fetch();29const body = await response.json();30capturedToken = body.verificationToken;31await route.fulfill({ response });32});3334await page.goto('/register');35await page.getByLabel('Name').fill('New User');36await page.getByLabel('Email').fill('[email protected]');37await page.getByLabel('Password', { exact: true }).fill('SecurePass!');38await page.getByLabel('Confirm password').fill('SecurePass!');39await page.getByRole('button', { name: 'Create account' }).click();4041await expect(page.getByText('Check your inbox')).toBeVisible();4243expect(capturedToken).toBeTruthy();44await page.goto(`/verify?token=${capturedToken}`);4546await expect(page.getByText('Email confirmed')).toBeVisible();47});48```4950### Fully Mocked Verification5152```typescript53test('verifies email with mocked endpoints', async ({ page }) => {54const mockToken = 'test-verification-abc123';5556await page.route('**/api/auth/register', async (route) => {57await route.fulfill({58status: 200,59contentType: 'application/json',60body: JSON.stringify({ message: 'Verification sent', verificationToken: mockToken }),61});62});6364await page.route(`**/api/auth/verify?token=${mockToken}`, async (route) => {65await route.fulfill({66status: 200,67contentType: 'application/json',68body: JSON.stringify({ verified: true }),69});70});7172await page.goto('/register');73await page.getByLabel('Email').fill('[email protected]');74await page.getByLabel('Password', { exact: true }).fill('Password123!');75await page.getByRole('button', { name: 'Sign up' }).click();7677await expect(page.getByText('Check your inbox')).toBeVisible();7879await page.goto(`/verify?token=${mockToken}`);80await expect(page.getByText('Email confirmed')).toBeVisible();81});82```8384---8586## Password Reset8788### Complete Reset Flow8990```typescript91test('resets password through email link', async ({ page }) => {92let resetToken = '';9394await page.route('**/api/auth/forgot-password', async (route) => {95const response = await route.fetch();96const body = await response.json();97resetToken = body.resetToken;98await route.fulfill({ response });99});100101await page.goto('/forgot-password');102await page.getByLabel('Email').fill('[email protected]');103await page.getByRole('button', { name: 'Send link' }).click();104105await expect(page.getByText('Reset email sent')).toBeVisible();106107expect(resetToken).toBeTruthy();108await page.goto(`/reset-password?token=${resetToken}`);109110await page.getByLabel('New password', { exact: true }).fill('NewPassword456!');111await page.getByLabel('Confirm password').fill('NewPassword456!');112await page.getByRole('button', { name: 'Update password' }).click();113114await expect(page.getByText('Password updated')).toBeVisible();115});116```117118### Expired Token Handling119120```typescript121test('shows error for expired reset token', async ({ page }) => {122await page.goto('/reset-password?token=expired-token');123124await page.getByLabel('New password', { exact: true }).fill('NewPass!');125await page.getByLabel('Confirm password').fill('NewPass!');126await page.getByRole('button', { name: 'Update password' }).click();127128await expect(page.getByRole('alert')).toContainText(/expired|invalid/i);129});130```131132### Password Strength Validation133134```typescript135test('enforces password requirements on reset', async ({ page }) => {136await page.goto('/reset-password?token=valid-token');137138await page.getByLabel('New password', { exact: true }).fill('weak');139await page.getByLabel('Confirm password').fill('weak');140await page.getByRole('button', { name: 'Update password' }).click();141142await expect(page.getByText(/at least 8 characters/i)).toBeVisible();143});144```145146---147148## Session Timeout149150### Detecting Expired Sessions151152```typescript153test('redirects to signin after session expires', async ({ page, context }) => {154await page.goto('/signin');155await page.getByLabel('Email').fill('[email protected]');156await page.getByLabel('Password').fill('Password!');157await page.getByRole('button', { name: 'Sign in' }).click();158await expect(page).toHaveURL('/home');159160const cookies = await context.cookies();161const sessionCookie = cookies.find((c) => c.name.includes('session'));162163if (sessionCookie) {164await context.clearCookies({ name: sessionCookie.name });165}166167await page.goto('/profile');168await expect(page).toHaveURL(/\/signin/);169await expect(page.getByText(/session.*expired|sign in again/i)).toBeVisible();170});171```172173### Session Extension Warning174175```typescript176test('shows warning before session expires', async ({ page }) => {177await page.route('**/api/auth/session', async (route) => {178await route.fulfill({179status: 200,180contentType: 'application/json',181body: JSON.stringify({ valid: true, expiresIn: 60 }),182});183});184185await page.goto('/home');186187await expect(page.getByText(/session.*expir/i)).toBeVisible({ timeout: 10000 });188await expect(page.getByRole('button', { name: /extend|stay signed in/i })).toBeVisible();189});190```191192### Session Extension Action193194```typescript195test('extends session when user clicks extend', async ({ page }) => {196let sessionExtended = false;197198await page.route('**/api/auth/session', async (route) => {199await route.fulfill({200status: 200,201contentType: 'application/json',202body: JSON.stringify({ valid: true, expiresIn: 60 }),203});204});205206await page.route('**/api/auth/refresh', async (route) => {207sessionExtended = true;208await route.fulfill({209status: 200,210contentType: 'application/json',211body: JSON.stringify({ valid: true, expiresIn: 3600 }),212});213});214215await page.goto('/home');216217await expect(page.getByRole('button', { name: /extend|stay signed in/i })).toBeVisible({218timeout: 10000,219});220await page.getByRole('button', { name: /extend|stay signed in/i }).click();221222expect(sessionExtended).toBe(true);223await expect(page.getByText(/session.*expir/i)).not.toBeVisible();224});225```226227---228229## Remember Me Persistence230231### Persistent Session232233```typescript234test('persists session with remember me enabled', async ({ browser }) => {235const ctx1 = await browser.newContext();236const page1 = await ctx1.newPage();237238await page1.goto('/signin');239await page1.getByLabel('Email').fill('[email protected]');240await page1.getByLabel('Password').fill('Password!');241await page1.getByLabel('Keep me signed in').check();242await page1.getByRole('button', { name: 'Sign in' }).click();243244await expect(page1).toHaveURL('/home');245246const state = await ctx1.storageState();247await ctx1.close();248249const ctx2 = await browser.newContext({ storageState: state });250const page2 = await ctx2.newPage();251252await page2.goto('/home');253await expect(page2).toHaveURL('/home');254await expect(page2.getByText('Welcome')).toBeVisible();255256await ctx2.close();257});258```259260### Session-Only Login261262```typescript263test('session-only login does not persist across browser restarts', async ({ browser }) => {264const ctx1 = await browser.newContext();265const page1 = await ctx1.newPage();266267await page1.goto('/signin');268await page1.getByLabel('Email').fill('[email protected]');269await page1.getByLabel('Password').fill('Password!');270// Leave "Remember me" unchecked271await expect(page1.getByLabel('Keep me signed in')).not.toBeChecked();272await page1.getByRole('button', { name: 'Sign in' }).click();273274await expect(page1).toHaveURL('/home');275276// Only keep persistent cookies (filter out session cookies)277const cookies = await ctx1.cookies();278await ctx1.close();279280const persistentCookies = cookies.filter((c) => c.expires > 0);281const ctx2 = await browser.newContext();282await ctx2.addCookies(persistentCookies);283const page2 = await ctx2.newPage();284285await page2.goto('/home');286287// Should redirect to login since session was not persisted288await expect(page2).toHaveURL(/\/signin/);289290await ctx2.close();291});292```293294---295296## Logout Patterns297298### Standard Logout with Session Cleanup299300```typescript301test.use({ storageState: '.auth/user.json' });302303test('logs out and clears session', async ({ page, context }) => {304await page.goto('/home');305306await page.getByRole('button', { name: /account|menu/i }).click();307await page.getByRole('menuitem', { name: 'Sign out' }).click();308309await expect(page).toHaveURL('/signin');310311const cookies = await context.cookies();312const sessionCookies = cookies.filter((c) => c.name.includes('session') || c.name.includes('token'));313expect(sessionCookies).toHaveLength(0);314315await page.goto('/home');316await expect(page).toHaveURL(/\/signin/);317});318```319320### Logout from All Devices321322```typescript323test('logs out from all devices', async ({ page }) => {324let logoutAllCalled = false;325326await page.route('**/api/auth/logout-all', async (route) => {327logoutAllCalled = true;328await route.fulfill({329status: 200,330contentType: 'application/json',331body: JSON.stringify({ message: 'Logged out everywhere' }),332});333});334335await page.goto('/settings/security');336337await page.getByRole('button', { name: 'Sign out everywhere' }).click();338await page.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click();339340expect(logoutAllCalled).toBe(true);341await expect(page).toHaveURL(/\/signin/);342});343```344345---346347## Tips3483491. **Configure shorter session timeouts in test environments** — Enables testing timeout behavior without slow tests3502. **Test token expiration edge cases** — Expired tokens, invalid tokens, already-used tokens3513. **Verify cleanup on logout** — Check both cookies and localStorage are cleared3524. **Test the full flow end-to-end** — Password reset should verify login with new password works353354---355356## Related357358- [authentication.md](authentication.md) — Storage state, OAuth mocking, MFA, role-based access, API login359- [fixtures-hooks.md](../core/fixtures-hooks.md) — Creating auth fixtures360- [third-party.md](./third-party.md) — Mocking external auth providers361