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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Use
storageStatefor 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.Never hard-code credentials in test files. Use environment variables or a
.envfile loaded viadotenv. Inplaywright.config.ts, setprocess.envvalues or use theenvoption inuse.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.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.
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
patterns/page-objects.md-- Encapsulate login flows in page objectsfoundations/assertions.md-- Assertion patterns for auth statesrecipes/crud-testing.md-- Test CRUD operations once authenticated