Search across all documentation pages
Handle authentication, API testing, route mocking, multi-tab scenarios, mobile viewports, and accessibility audits.
Quick-reference recipe card -- copy-paste ready.
import { test, expect } from "@playwright/test";
// Shared authentication state
test.use({ storageState: "e2e/.auth/user.json" });
// API testing
const response = await request.get("/api/users");
expect(response.ok()).toBe(true);
// Mock API routes
await page.route("/api/products", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 1, name: "Widget" }]),
});
});
// Mobile viewport
test.use({ ...devices["iPhone 14"] });
// Accessibility testing
import AxeBuilder from "@axe-core/playwright";
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);When to reach for this: When your E2E tests need to handle login sessions, test APIs directly, mock network responses, or verify accessibility.
// e2e/auth.setup.ts
import { test as setup, expect } from "@playwright/test";
const authFile = "e2e/.auth/user.json";
setup("authenticate", async ({ page }) => {
// Navigate to login page
await page.goto("/login"
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
projects: [
// Setup project runs first to authenticate
{
name: "setup",
testMatch: /.*\.setup\.ts/,
// e2e/dashboard.spec.ts
import { test, expect } from "@playwright/test";
// All tests in this file are authenticated (via project config)
test.describe("Authenticated Dashboard", () => {
test("shows user name in header", async ({ page }) => {
await page.goto(
// e2e/api.spec.ts
import { test, expect } from "@playwright/test";
test.describe("API Tests", () => {
test("GET /api/products returns products", async ({ request }) => {
const response = await request.get(
// e2e/mocked.spec.ts
import { test, expect } from "@playwright/test";
test.describe("With Mocked API", () => {
test("shows empty state when no products", async ({ page }) => {
// Mock the API to return empty array
await page.route("/api/products"
// e2e/accessibility.spec.ts
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test.describe("Accessibility", () => {
test("home page has no a11y violations", async ({ page }) => {
await
What this demonstrates:
request fixturepage.route() for controlled scenariosstorageState saves and restores cookies and localStorage, so authenticated tests do not repeat logindependencies arraypage.route() intercepts network requests matching a URL pattern and lets you fulfill, abort, or modify themrequest is a Playwright fixture for making HTTP requests directly -- useful for API testing without a browser pageAxeBuilder runs the axe-core accessibility engine inside the page and returns violations with detailed metadataTesting across multiple tabs:
test("opens product in new tab", async ({ page, context }) => {
await page.goto("/products");
// Listen for new page (tab)
const newPagePromise = context.waitForEvent("page");
await page.getByRole
Testing with different viewports:
test.describe("mobile navigation", () => {
test.use({
viewport: { width: 375, height: 667 },
isMobile: true,
hasTouch: true,
});
test("shows hamburger menu", async ({ page
Multiple authentication roles:
// playwright.config.ts
projects: [
{ name: "admin-setup", testMatch: /admin\.setup\.ts/ },
{ name: "user-setup", testMatch: /user\.setup\.ts/ },
{
name:
// Typed route handler
await page.route("/api/users", async (route: Route) => {
const method = route.request().method();
if (method === "GET") {
await route.fulfill
storageState file in git -- The auth state file contains session tokens. Fix: Add e2e/.auth/ to .gitignore.
Auth state expiring -- If sessions expire quickly, the setup may need to run more frequently. Fix: Use long-lived test tokens or run setup before each test file with test.beforeEach.
page.route must be called before navigation -- Routes set after page.goto() do not intercept the initial page load requests. Fix: Call page.route() before page.goto().
axe-core false positives -- Some violations may be intentional (e.g., color contrast on branded elements). Fix: Use .exclude() for known exceptions or .disableRules() for specific rules:
new AxeBuilder(\{ page \})
.exclude("#brand-banner")
.disableRules(["color-contrast"])
.| Alternative | Use When | Don't Use When |
|---|---|---|
| Cypress intercept | You use Cypress and need route mocking | You use Playwright |
| Lighthouse CI | You need performance and a11y audits in CI | You want granular a11y violation assertions |
| jest-axe | You want a11y testing in unit tests (jsdom) | You need real browser rendering for accurate results |
| Supertest | You want API integration tests without a browser | You need browser context (cookies, auth) |
storageState.storageState auth file be committed to git?No. It contains session tokens. Add e2e/.auth/ to .gitignore.
await page.route("/api/products", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 1, name: "Widget" }]),
});
request fixture used for?It makes HTTP requests directly without a browser page -- useful for API testing:
const response = await request.get("/api/products");
expect(response.ok()).toBe(true);Install @axe-core/playwright and use:
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(results.violations).toEqual([]);Create separate setup projects and storage state files for each role. Use testMatch patterns to route tests to the correct project:
{ name: "admin-tests", use: { storageState: "e2e/.auth/admin.json" } }page.route() be called before page.goto()?Routes set after navigation do not intercept the initial page load requests. Always register route mocks before navigating.
const newPagePromise = context.waitForEvent("page");
await page.getByRole("link").click({ modifiers: ["Meta"] });
const newPage = await newPagePromise;
await newPage.waitForLoadState();test.use({
viewport: { width: 375, height: 667 },
isMobile: true,
hasTouch: true,
});Or use device presets: test.use({ ...devices["iPhone 14"] }).
Exclude specific elements or disable rules:
new AxeBuilder({ page })
.exclude("#brand-banner")
.disableRules(["color-contrast"])
.analyze();await page.route("/api/users", async (route: Route) => {
const method = route.request().method();
if (method === "GET") {
await route.fulfill
request fixture use storageState by default?No. Use request.newContext({ storageState: "e2e/.auth/user.json" }) or configure it in the project settings.
API tests and authentication -- The request fixture does not use storageState by default. Fix: Use request.newContext({ storageState: "e2e/.auth/user.json" }) or set it in the project config.
Call page.route() before page.goto().