# 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<Role, { email: string; password: string }> = {
    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
