# Authentication Flow Recipes > **When to use**: You need to test login, signup, logout, session management, or any authentication-related user flow. --- ## Recipe 1: Basic Login ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("logs in with valid credentials", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("SecurePass123!") await page.getByRole("button", { name: "Sign in" }).click() // Wait for navigation to complete after login await expect(page).toHaveURL("/dashboard") await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible() await expect(page.getByText("Welcome, user@example.com")).toBeVisible() }) test("shows error for invalid credentials", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("WrongPassword") await page.getByRole("button", { name: "Sign in" }).click() await expect(page.getByRole("alert")).toContainText("Invalid email or password") // Should remain on login page await expect(page).toHaveURL("/login") }) test("shows validation errors for empty fields", async ({ page }) => { await page.goto("/login") await page.getByRole("button", { name: "Sign in" }).click() await expect(page.getByText("Email is required")).toBeVisible() await expect(page.getByText("Password is required")).toBeVisible() }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") test("logs in with valid credentials", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("SecurePass123!") await page.getByRole("button", { name: "Sign in" }).click() await expect(page).toHaveURL("/dashboard") await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible() await expect(page.getByText("Welcome, user@example.com")).toBeVisible() }) test("shows error for invalid credentials", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("WrongPassword") await page.getByRole("button", { name: "Sign in" }).click() await expect(page.getByRole("alert")).toContainText("Invalid email or password") await expect(page).toHaveURL("/login") }) test("shows validation errors for empty fields", async ({ page }) => { await page.goto("/login") await page.getByRole("button", { name: "Sign in" }).click() await expect(page.getByText("Email is required")).toBeVisible() await expect(page.getByText("Password is required")).toBeVisible() }) ``` --- ## Recipe 2: Login with "Remember Me" ### Complete Example **TypeScript** ```typescript import { test, expect, type BrowserContext } from "@playwright/test" test("remember me persists session across browser restarts", async ({ browser }) => { // First session: log in with "remember me" checked const context1 = await browser.newContext() const page1 = await context1.newPage() await page1.goto("/login") await page1.getByLabel("Email").fill("user@example.com") await page1.getByLabel("Password").fill("SecurePass123!") await page1.getByLabel("Remember me").check() await page1.getByRole("button", { name: "Sign in" }).click() await expect(page1).toHaveURL("/dashboard") // Save storage state (cookies + localStorage) const storageState = await context1.storageState() await context1.close() // Second session: restore state to simulate browser restart const context2 = await browser.newContext({ storageState }) const page2 = await context2.newPage() await page2.goto("/dashboard") // Should still be logged in without re-authenticating await expect(page2).toHaveURL("/dashboard") await expect(page2.getByText("Welcome, user@example.com")).toBeVisible() await context2.close() }) test("no remember me does not persist session", async ({ browser }) => { const context1 = await browser.newContext() const page1 = await context1.newPage() await page1.goto("/login") await page1.getByLabel("Email").fill("user@example.com") await page1.getByLabel("Password").fill("SecurePass123!") // Explicitly leave "Remember me" unchecked await expect(page1.getByLabel("Remember me")).not.toBeChecked() await page1.getByRole("button", { name: "Sign in" }).click() await expect(page1).toHaveURL("/dashboard") // Capture only cookies (not sessionStorage) to simulate new tab/window const cookies = await context1.cookies() await context1.close() // New context with only persistent cookies (session cookies are excluded) const persistentCookies = cookies.filter((c) => c.expires > 0) const context2 = await browser.newContext() await context2.addCookies(persistentCookies) const page2 = await context2.newPage() await page2.goto("/dashboard") // Should redirect to login since session was not persisted await expect(page2).toHaveURL(/\/login/) await context2.close() }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") test("remember me persists session across browser restarts", async ({ browser }) => { const context1 = await browser.newContext() const page1 = await context1.newPage() await page1.goto("/login") await page1.getByLabel("Email").fill("user@example.com") await page1.getByLabel("Password").fill("SecurePass123!") await page1.getByLabel("Remember me").check() await page1.getByRole("button", { name: "Sign in" }).click() await expect(page1).toHaveURL("/dashboard") const storageState = await context1.storageState() await context1.close() const context2 = await browser.newContext({ storageState }) const page2 = await context2.newPage() await page2.goto("/dashboard") await expect(page2).toHaveURL("/dashboard") await expect(page2.getByText("Welcome, user@example.com")).toBeVisible() await context2.close() }) ``` --- ## Recipe 3: Signup with Email Verification (Mocked) ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("completes signup flow with mocked email verification", async ({ page }) => { // Intercept the verification email API so we can capture the token let verificationToken = "" await page.route("**/api/auth/send-verification", async (route) => { // Let the request go through to the server const response = await route.fetch() const body = await response.json() // Capture the token from the API response (test environments expose this) verificationToken = body.verificationToken await route.fulfill({ response }) }) // Step 1: Fill out signup form await page.goto("/signup") await page.getByLabel("Full name").fill("Jane Tester") await page.getByLabel("Email").fill("jane@example.com") await page.getByLabel("Password", { exact: true }).fill("SecurePass123!") await page.getByLabel("Confirm password").fill("SecurePass123!") await page.getByLabel("I agree to the Terms of Service").check() await page.getByRole("button", { name: "Create account" }).click() // Step 2: Verify "check your email" screen appears await expect(page.getByText("Check your email")).toBeVisible() await expect(page.getByText("jane@example.com")).toBeVisible() // Step 3: Navigate directly to verification URL using captured token expect(verificationToken).toBeTruthy() await page.goto(`/verify-email?token=${verificationToken}`) // Step 4: Verify account is now active await expect(page.getByText("Email verified successfully")).toBeVisible() await expect(page).toHaveURL("/login") // Step 5: Log in with the new account await page.getByLabel("Email").fill("jane@example.com") await page.getByLabel("Password").fill("SecurePass123!") await page.getByRole("button", { name: "Sign in" }).click() await expect(page).toHaveURL("/dashboard") await expect(page.getByText("Welcome, Jane Tester")).toBeVisible() }) test("signup with fully mocked email API (no server dependency)", async ({ page }) => { const fakeToken = "test-verification-token-12345" // Mock the signup API endpoint await page.route("**/api/auth/signup", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: "Verification email sent", verificationToken: fakeToken, }), }) }) // Mock the verification endpoint await page.route(`**/api/auth/verify-email?token=${fakeToken}`, async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: "Email verified", redirectTo: "/login" }), }) }) await page.goto("/signup") await page.getByLabel("Full name").fill("Jane Tester") await page.getByLabel("Email").fill("jane@example.com") await page.getByLabel("Password", { exact: true }).fill("SecurePass123!") await page.getByLabel("Confirm password").fill("SecurePass123!") await page.getByLabel("I agree to the Terms of Service").check() await page.getByRole("button", { name: "Create account" }).click() await expect(page.getByText("Check your email")).toBeVisible() // Simulate clicking the verification link from the email await page.goto(`/verify-email?token=${fakeToken}`) await expect(page.getByText("Email verified successfully")).toBeVisible() }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") test("completes signup flow with mocked email verification", async ({ page }) => { let verificationToken = "" await page.route("**/api/auth/send-verification", async (route) => { const response = await route.fetch() const body = await response.json() verificationToken = body.verificationToken await route.fulfill({ response }) }) await page.goto("/signup") await page.getByLabel("Full name").fill("Jane Tester") await page.getByLabel("Email").fill("jane@example.com") await page.getByLabel("Password", { exact: true }).fill("SecurePass123!") await page.getByLabel("Confirm password").fill("SecurePass123!") await page.getByLabel("I agree to the Terms of Service").check() await page.getByRole("button", { name: "Create account" }).click() await expect(page.getByText("Check your email")).toBeVisible() expect(verificationToken).toBeTruthy() await page.goto(`/verify-email?token=${verificationToken}`) await expect(page.getByText("Email verified successfully")).toBeVisible() await expect(page).toHaveURL("/login") }) ``` --- ## Recipe 4: Password Reset Flow ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("completes password reset flow", async ({ page }) => { let resetToken = "" // Intercept the password reset API to capture the reset token 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 }) }) // Step 1: Request password reset await page.goto("/forgot-password") await page.getByLabel("Email").fill("user@example.com") await page.getByRole("button", { name: "Send reset link" }).click() await expect(page.getByText("Reset link sent")).toBeVisible() await expect(page.getByText("user@example.com")).toBeVisible() // Step 2: Navigate to the reset page with token expect(resetToken).toBeTruthy() await page.goto(`/reset-password?token=${resetToken}`) // Step 3: Set new password await page.getByLabel("New password", { exact: true }).fill("NewSecurePass456!") await page.getByLabel("Confirm new password").fill("NewSecurePass456!") await page.getByRole("button", { name: "Reset password" }).click() await expect(page.getByText("Password reset successfully")).toBeVisible() // Step 4: Log in with new password await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("NewSecurePass456!") await page.getByRole("button", { name: "Sign in" }).click() await expect(page).toHaveURL("/dashboard") }) test("password reset with expired token shows error", async ({ page }) => { await page.goto("/reset-password?token=expired-token-123") await page.getByLabel("New password", { exact: true }).fill("NewSecurePass456!") await page.getByLabel("Confirm new password").fill("NewSecurePass456!") await page.getByRole("button", { name: "Reset password" }).click() await expect(page.getByRole("alert")).toContainText(/token has expired|link is no longer valid/i) await expect(page.getByRole("link", { name: "Request a new reset link" })).toBeVisible() }) test("password reset enforces password strength requirements", async ({ page }) => { await page.goto("/reset-password?token=valid-token") // Try a weak password await page.getByLabel("New password", { exact: true }).fill("123") await page.getByLabel("Confirm new password").fill("123") await page.getByRole("button", { name: "Reset password" }).click() await expect(page.getByText(/at least 8 characters/i)).toBeVisible() }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") test("completes password reset flow", 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("user@example.com") await page.getByRole("button", { name: "Send reset link" }).click() await expect(page.getByText("Reset link sent")).toBeVisible() expect(resetToken).toBeTruthy() await page.goto(`/reset-password?token=${resetToken}`) await page.getByLabel("New password", { exact: true }).fill("NewSecurePass456!") await page.getByLabel("Confirm new password").fill("NewSecurePass456!") await page.getByRole("button", { name: "Reset password" }).click() await expect(page.getByText("Password reset successfully")).toBeVisible() await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("NewSecurePass456!") await page.getByRole("button", { name: "Sign in" }).click() await expect(page).toHaveURL("/dashboard") }) ``` --- ## Recipe 5: OAuth Login (Mocked Callback) ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("logs in via mocked Google OAuth callback", async ({ page }) => { // Intercept the OAuth redirect to Google and simulate the callback await page.route("**/accounts.google.com/**", async (route) => { // Instead of actually going to Google, redirect back to our callback URL // with a mock authorization code const callbackUrl = new URL("/auth/callback/google", "http://localhost:3000") callbackUrl.searchParams.set("code", "mock-auth-code-12345") callbackUrl.searchParams.set( "state", route .request() .url() .match(/state=([^&]+)/)?.[1] || "" ) await route.fulfill({ status: 302, headers: { location: callbackUrl.toString() }, }) }) // Mock the token exchange on our backend await page.route("**/api/auth/callback/google**", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ user: { id: "1", email: "googleuser@gmail.com", name: "Google User", avatar: "https://example.com/avatar.jpg", }, token: "mock-jwt-token", }), }) }) await page.goto("/login") // Click the OAuth button await page.getByRole("button", { name: /Sign in with Google/i }).click() // After the mocked OAuth flow, we should land on the dashboard await expect(page).toHaveURL("/dashboard") await expect(page.getByText("Google User")).toBeVisible() }) test("handles OAuth login failure gracefully", async ({ page }) => { // Simulate OAuth provider returning an error await page.route("**/accounts.google.com/**", async (route) => { const callbackUrl = new URL("/auth/callback/google", "http://localhost:3000") callbackUrl.searchParams.set("error", "access_denied") callbackUrl.searchParams.set("error_description", "User denied access") await route.fulfill({ status: 302, headers: { location: callbackUrl.toString() }, }) }) await page.goto("/login") await page.getByRole("button", { name: /Sign in with Google/i }).click() await expect(page.getByRole("alert")).toContainText(/authentication failed|access denied/i) await expect(page).toHaveURL(/\/login/) }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") test("logs in via mocked Google OAuth callback", async ({ page }) => { await page.route("**/accounts.google.com/**", async (route) => { const callbackUrl = new URL("/auth/callback/google", "http://localhost:3000") callbackUrl.searchParams.set("code", "mock-auth-code-12345") callbackUrl.searchParams.set( "state", route .request() .url() .match(/state=([^&]+)/)?.[1] || "" ) await route.fulfill({ status: 302, headers: { location: callbackUrl.toString() }, }) }) await page.route("**/api/auth/callback/google**", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ user: { id: "1", email: "googleuser@gmail.com", name: "Google User" }, token: "mock-jwt-token", }), }) }) await page.goto("/login") await page.getByRole("button", { name: /Sign in with Google/i }).click() await expect(page).toHaveURL("/dashboard") await expect(page.getByText("Google User")).toBeVisible() }) ``` --- ## Recipe 6: Role-Based Access Testing ### Complete Example **TypeScript** ```typescript import { test, expect, type Page } from "@playwright/test" import path from "path" // Store auth state for each role in separate files const authDir = path.resolve(__dirname, "../.auth") // Setup: create auth state files for each role (run in globalSetup or auth.setup) // This pattern uses Playwright's storage state for efficient role switching const roles = ["admin", "editor", "viewer"] as const type Role = (typeof roles)[number] function authFile(role: Role): string { return path.join(authDir, `${role}.json`) } // Auth setup project — runs before all tests test.describe("auth setup", () => { const credentials: Record = { admin: { email: "admin@example.com", password: "AdminPass123!" }, editor: { email: "editor@example.com", password: "EditorPass123!" }, viewer: { email: "viewer@example.com", password: "ViewerPass123!" }, } for (const role of roles) { test(`authenticate as ${role}`, async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill(credentials[role].email) await page.getByLabel("Password").fill(credentials[role].password) await page.getByRole("button", { name: "Sign in" }).click() await expect(page).toHaveURL("/dashboard") await page.context().storageState({ path: authFile(role) }) }) } }) // Admin tests test.describe("admin access", () => { test.use({ storageState: authFile("admin") }) test("admin can access user management", async ({ page }) => { await page.goto("/admin/users") await expect(page).toHaveURL("/admin/users") await expect(page.getByRole("heading", { name: "User Management" })).toBeVisible() await expect(page.getByRole("button", { name: "Add user" })).toBeVisible() await expect(page.getByRole("button", { name: "Delete" }).first()).toBeVisible() }) test("admin can access system settings", async ({ page }) => { await page.goto("/admin/settings") await expect(page).toHaveURL("/admin/settings") await expect(page.getByRole("heading", { name: "System Settings" })).toBeVisible() }) }) // Editor tests test.describe("editor access", () => { test.use({ storageState: authFile("editor") }) test("editor can create and edit content", async ({ page }) => { await page.goto("/content") await expect(page.getByRole("button", { name: "New post" })).toBeVisible() await expect(page.getByRole("button", { name: "Edit" }).first()).toBeVisible() }) test("editor cannot access admin area", async ({ page }) => { await page.goto("/admin/users") // Should be redirected or shown forbidden await expect(page).toHaveURL(/\/(403|dashboard|login)/) }) }) // Viewer tests test.describe("viewer access", () => { test.use({ storageState: authFile("viewer") }) test("viewer can read content", async ({ page }) => { await page.goto("/content") await expect(page.getByRole("article").first()).toBeVisible() }) test("viewer cannot create content", async ({ page }) => { await page.goto("/content") await expect(page.getByRole("button", { name: "New post" })).not.toBeVisible() }) test("viewer cannot access admin area", async ({ page }) => { await page.goto("/admin/users") await expect(page).toHaveURL(/\/(403|dashboard|login)/) }) }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") const path = require("path") const authDir = path.resolve(__dirname, "../.auth") function authFile(role) { return path.join(authDir, `${role}.json`) } // Admin tests test.describe("admin access", () => { test.use({ storageState: authFile("admin") }) test("admin can access user management", async ({ page }) => { await page.goto("/admin/users") await expect(page).toHaveURL("/admin/users") await expect(page.getByRole("heading", { name: "User Management" })).toBeVisible() await expect(page.getByRole("button", { name: "Add user" })).toBeVisible() }) test("admin can access system settings", async ({ page }) => { await page.goto("/admin/settings") await expect(page).toHaveURL("/admin/settings") }) }) // Viewer tests test.describe("viewer access", () => { test.use({ storageState: authFile("viewer") }) test("viewer cannot create content", async ({ page }) => { await page.goto("/content") await expect(page.getByRole("button", { name: "New post" })).not.toBeVisible() }) test("viewer cannot access admin area", async ({ page }) => { await page.goto("/admin/users") await expect(page).toHaveURL(/\/(403|dashboard|login)/) }) }) ``` --- ## Recipe 7: Session Timeout Handling ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("redirects to login after session timeout", async ({ page, context }) => { // Log in first await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("SecurePass123!") await page.getByRole("button", { name: "Sign in" }).click() await expect(page).toHaveURL("/dashboard") // Simulate session expiration by clearing the session cookie const cookies = await context.cookies() const sessionCookie = cookies.find( (c) => c.name === "session" || c.name === "sid" || c.name === "connect.sid" ) if (sessionCookie) { await context.clearCookies({ name: sessionCookie.name }) } // Try to navigate to a protected page await page.goto("/settings") // Should redirect to login with a return URL await expect(page).toHaveURL(/\/login/) await expect(page.getByText(/session.*expired|please.*log in again/i)).toBeVisible() }) test("shows session expiry warning before timeout", async ({ page }) => { // Mock the session check endpoint to return "expiring soon" await page.route("**/api/auth/session", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ valid: true, expiresIn: 120, // 2 minutes remaining }), }) }) await page.goto("/dashboard") // The app should show a warning when session is about to expire await expect(page.getByText(/session.*expir/i)).toBeVisible({ timeout: 10000 }) await expect(page.getByRole("button", { name: /extend|stay logged 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: 120 }), }) }) 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("/dashboard") await expect(page.getByRole("button", { name: /extend|stay logged in/i })).toBeVisible({ timeout: 10000, }) await page.getByRole("button", { name: /extend|stay logged in/i }).click() expect(sessionExtended).toBe(true) await expect(page.getByText(/session.*expir/i)).not.toBeVisible() }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") test("redirects to login after session timeout", async ({ page, context }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("SecurePass123!") await page.getByRole("button", { name: "Sign in" }).click() await expect(page).toHaveURL("/dashboard") const cookies = await context.cookies() const sessionCookie = cookies.find( (c) => c.name === "session" || c.name === "sid" || c.name === "connect.sid" ) if (sessionCookie) { await context.clearCookies({ name: sessionCookie.name }) } await page.goto("/settings") await expect(page).toHaveURL(/\/login/) await expect(page.getByText(/session.*expired|please.*log in again/i)).toBeVisible() }) ``` --- ## Recipe 8: Logout ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" // Use a logged-in state test.use({ storageState: ".auth/user.json" }) test("logs out and redirects to login page", async ({ page }) => { await page.goto("/dashboard") await expect(page.getByText("Welcome")).toBeVisible() // Open user menu and click logout await page.getByRole("button", { name: /user menu|account|profile/i }).click() await page.getByRole("menuitem", { name: "Log out" }).click() // Should redirect to login await expect(page).toHaveURL("/login") // Verify we can no longer access protected pages await page.goto("/dashboard") await expect(page).toHaveURL(/\/login/) }) test("logout clears all session data", async ({ page, context }) => { await page.goto("/dashboard") await page.getByRole("button", { name: /user menu|account|profile/i }).click() await page.getByRole("menuitem", { name: "Log out" }).click() await expect(page).toHaveURL("/login") // Verify cookies are cleared const cookies = await context.cookies() const sessionCookies = cookies.filter( (c) => c.name === "session" || c.name === "sid" || c.name === "token" ) expect(sessionCookies).toHaveLength(0) // Verify localStorage is cleared const token = await page.evaluate(() => localStorage.getItem("authToken")) expect(token).toBeNull() }) test("logout 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 from all devices" }), }) }) await page.goto("/settings/security") await page.getByRole("button", { name: "Log out of all devices" }).click() // Confirm the action in the dialog await page.getByRole("dialog").getByRole("button", { name: "Confirm" }).click() expect(logoutAllCalled).toBe(true) await expect(page).toHaveURL(/\/login/) }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") test.use({ storageState: ".auth/user.json" }) test("logs out and redirects to login page", async ({ page }) => { await page.goto("/dashboard") await page.getByRole("button", { name: /user menu|account|profile/i }).click() await page.getByRole("menuitem", { name: "Log out" }).click() await expect(page).toHaveURL("/login") await page.goto("/dashboard") await expect(page).toHaveURL(/\/login/) }) test("logout clears all session data", async ({ page, context }) => { await page.goto("/dashboard") await page.getByRole("button", { name: /user menu|account|profile/i }).click() await page.getByRole("menuitem", { name: "Log out" }).click() await expect(page).toHaveURL("/login") const cookies = await context.cookies() const sessionCookies = cookies.filter( (c) => c.name === "session" || c.name === "sid" || c.name === "token" ) expect(sessionCookies).toHaveLength(0) }) ``` --- ## Variations ### Login via API for Speed Skip the UI for login when testing non-auth features. Use `request` context to get tokens faster. **TypeScript** ```typescript import { test as setup } from "@playwright/test" setup("authenticate via API", async ({ request }) => { const response = await request.post("/api/auth/login", { data: { email: "user@example.com", password: "SecurePass123!", }, }) expect(response.ok()).toBeTruthy() // Save the auth state await request.storageState({ path: ".auth/user.json" }) }) ``` ### Multi-Factor Authentication ```typescript test("logs in with MFA (TOTP)", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("mfa-user@example.com") await page.getByLabel("Password").fill("SecurePass123!") await page.getByRole("button", { name: "Sign in" }).click() // MFA challenge screen appears await expect(page.getByText("Enter verification code")).toBeVisible() // In test environments, use a predictable TOTP code or mock the verification await page.route("**/api/auth/verify-mfa", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ success: true, token: "jwt-token" }), }) }) await page.getByLabel("Verification code").fill("123456") await page.getByRole("button", { name: "Verify" }).click() await expect(page).toHaveURL("/dashboard") }) ``` ### Testing Auth with Different Browser Contexts ```typescript test("two users interact simultaneously", async ({ browser }) => { const adminContext = await browser.newContext({ storageState: ".auth/admin.json", }) const userContext = await browser.newContext({ storageState: ".auth/viewer.json", }) const adminPage = await adminContext.newPage() const userPage = await userContext.newPage() // Admin bans a user await adminPage.goto("/admin/users") await adminPage .getByRole("row", { name: "viewer@example.com" }) .getByRole("button", { name: "Ban" }) .click() await adminPage.getByRole("dialog").getByRole("button", { name: "Confirm" }).click() // User tries to navigate and should be blocked await userPage.goto("/dashboard") await expect(userPage.getByText(/account.*banned|access.*revoked/i)).toBeVisible() await adminContext.close() await userContext.close() }) ``` --- ## Tips 1. **Use `storageState` for reusable auth**. Run login once in a setup project, save the state to a JSON file, and reuse it across all tests. This dramatically speeds up your test suite since you only log in once per role. 2. **Never hard-code credentials in test files**. Use environment variables or a `.env` file loaded via `dotenv`. In `playwright.config.ts`, set `process.env` values or use the `env` option in `use`. 3. **Prefer API-based login for non-auth tests**. Only test the login UI in your auth-specific tests. For everything else, authenticate via the API or by restoring `storageState`. This makes tests faster and less brittle. 4. **Test the negative paths too**. Invalid credentials, expired tokens, locked accounts, rate limiting. These are the scenarios users actually encounter in production and are frequently undertested. 5. **Set a shorter session timeout in your test environment**. If your app has a 30-minute session timeout, configure it to 30 seconds in the test environment so you can test timeout behavior without slow tests. --- ## Related - [Playwright Authentication Docs](https://playwright.dev/docs/auth) - `patterns/page-objects.md` -- Encapsulate login flows in page objects - `foundations/assertions.md` -- Assertion patterns for auth states - `recipes/crud-testing.md` -- Test CRUD operations once authenticated