Playwright
"Playwright: end-to-end testing, browser automation, selectors, assertions, fixtures, page objects, visual regression, CI, codegen"
Playwright is a cross-browser end-to-end testing framework built by Microsoft. It treats the browser as a first-class automation target, providing reliable, fast, and capable testing across Chromium, Firefox, and WebKit with a single API. Tests run in isolated browser contexts, ensuring zero interference between tests. Playwright auto-waits for elements to be actionable before performing operations, eliminating the most common source of flaky tests. Every test gets a fresh browser context, equivalent to a brand-new browser profile, making isolation the default rather than an afterthought.
## Key Points
- Use role-based and label-based locators over CSS/XPath selectors for resilience and accessibility alignment.
- Leverage auto-waiting rather than adding manual `waitForTimeout` calls.
- Keep browser state isolated with per-test contexts; share authentication state via `storageState` files.
- Use `test.describe.configure({ mode: "serial" })` sparingly and only when tests truly depend on order.
- Run `npx playwright codegen` to bootstrap selectors and interactions, then refine them in code.
- Capture traces on first retry (`trace: "on-first-retry"`) to debug failures without re-running.
- Pin visual regression baselines per platform and update them intentionally.
- Avoid `page.waitForTimeout(ms)` as a synchronization mechanism; rely on locator assertions and auto-wait instead.
- Do not use fragile CSS selectors like `.btn-primary > span:nth-child(2)`; they break on minor markup changes.
- Do not share mutable state between tests. Each test must be independently runnable.
- Avoid testing third-party services (payment gateways, OAuth providers) end-to-end; mock them at the network layer.
- Do not skip cross-browser testing in CI; bugs that only appear in WebKit or Firefox will reach production.skilldb get testing-services-skills/PlaywrightFull skill: 318 linesPlaywright
Core Philosophy
Playwright is a cross-browser end-to-end testing framework built by Microsoft. It treats the browser as a first-class automation target, providing reliable, fast, and capable testing across Chromium, Firefox, and WebKit with a single API. Tests run in isolated browser contexts, ensuring zero interference between tests. Playwright auto-waits for elements to be actionable before performing operations, eliminating the most common source of flaky tests. Every test gets a fresh browser context, equivalent to a brand-new browser profile, making isolation the default rather than an afterthought.
Setup
Installation and Configuration
// Install Playwright and browsers
// npm init playwright@latest
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
["html", { open: "never" }],
["junit", { outputFile: "results.xml" }],
],
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
{ name: "mobile-chrome", use: { ...devices["Pixel 5"] } },
],
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
Project Structure
e2e/
fixtures/
auth.fixture.ts
db.fixture.ts
pages/
login.page.ts
dashboard.page.ts
specs/
auth.spec.ts
dashboard.spec.ts
utils/
test-data.ts
Key Techniques
Selectors and Locators
import { test, expect } from "@playwright/test";
test("use recommended locator strategies", async ({ page }) => {
await page.goto("/signup");
// Preferred: role-based selectors (accessibility-first)
await page.getByRole("heading", { name: "Create Account" }).isVisible();
await page.getByRole("textbox", { name: "Email" }).fill("user@test.com");
await page.getByRole("button", { name: "Sign Up" }).click();
// Labels and placeholders
await page.getByLabel("Password").fill("secure-pass-123");
await page.getByPlaceholder("Enter your name").fill("Jane");
// Test IDs for elements without semantic roles
await page.getByTestId("avatar-upload").setInputFiles("avatar.png");
// Text content
await page.getByText("Terms of Service").click();
// Filtering and chaining locators
const row = page.getByRole("row").filter({ hasText: "admin@test.com" });
await row.getByRole("button", { name: "Edit" }).click();
});
Assertions
test("web-first assertions with auto-wait", async ({ page }) => {
await page.goto("/dashboard");
// These auto-retry until the condition is met or timeout
await expect(page.getByRole("heading")).toHaveText("Dashboard");
await expect(page.getByTestId("user-count")).toHaveText(/\d+ users/);
await expect(page.getByRole("alert")).not.toBeVisible();
await expect(page).toHaveURL(/.*dashboard/);
await expect(page).toHaveTitle("My App - Dashboard");
// Soft assertions (don't stop the test on failure)
await expect.soft(page.getByTestId("metric-a")).toHaveText("42");
await expect.soft(page.getByTestId("metric-b")).toHaveText("99");
});
Custom Fixtures
// e2e/fixtures/auth.fixture.ts
import { test as base, expect } from "@playwright/test";
type AuthFixtures = {
authenticatedPage: ReturnType<typeof base["extend"]> extends infer T ? T : never;
adminPage: ReturnType<typeof base["extend"]> extends infer T ? T : never;
};
export const test = base.extend<{
userPage: Awaited<ReturnType<typeof createAuthPage>>;
adminPage: Awaited<ReturnType<typeof createAuthPage>>;
}>({
userPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: "e2e/.auth/user.json",
});
const page = await context.newPage();
await use(page);
await context.close();
},
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: "e2e/.auth/admin.json",
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
async function createAuthPage(browser: any, stateFile: string) {
const ctx = await browser.newContext({ storageState: stateFile });
return ctx.newPage();
}
export { expect };
Page Object Model
// e2e/pages/login.page.ts
import { type Page, type Locator, expect } from "@playwright/test";
export class LoginPage {
private readonly emailInput: Locator;
private readonly passwordInput: Locator;
private readonly submitButton: Locator;
private readonly errorAlert: Locator;
constructor(private readonly page: Page) {
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.submitButton = page.getByRole("button", { name: "Sign In" });
this.errorAlert = page.getByRole("alert");
}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorAlert).toHaveText(message);
}
async expectRedirectToDashboard() {
await expect(this.page).toHaveURL(/.*dashboard/);
}
}
// e2e/specs/auth.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/login.page";
test.describe("Authentication", () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test("successful login redirects to dashboard", async () => {
await loginPage.login("user@test.com", "password123");
await loginPage.expectRedirectToDashboard();
});
test("invalid credentials show error", async () => {
await loginPage.login("user@test.com", "wrong-password");
await loginPage.expectError("Invalid email or password");
});
});
API Testing and Network Interception
test("mock API responses", async ({ page }) => {
// Intercept and mock API calls
await page.route("**/api/users", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]),
})
);
await page.goto("/users");
await expect(page.getByRole("listitem")).toHaveCount(2);
});
test("standalone API testing", async ({ request }) => {
const response = await request.post("/api/users", {
data: { name: "Charlie", email: "charlie@test.com" },
});
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user.name).toBe("Charlie");
});
Visual Regression Testing
test("visual regression for dashboard", async ({ page }) => {
await page.goto("/dashboard");
await page.waitForLoadState("networkidle");
// Full-page screenshot comparison
await expect(page).toHaveScreenshot("dashboard-full.png", {
maxDiffPixelRatio: 0.01,
});
// Component-level screenshot
const chart = page.getByTestId("revenue-chart");
await expect(chart).toHaveScreenshot("revenue-chart.png", {
animations: "disabled",
mask: [page.getByTestId("dynamic-timestamp")],
});
});
CI Configuration
# .github/workflows/e2e.yml
name: E2E Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 14
Best Practices
- Use role-based and label-based locators over CSS/XPath selectors for resilience and accessibility alignment.
- Leverage auto-waiting rather than adding manual
waitForTimeoutcalls. - Keep browser state isolated with per-test contexts; share authentication state via
storageStatefiles. - Use
test.describe.configure({ mode: "serial" })sparingly and only when tests truly depend on order. - Run
npx playwright codegento bootstrap selectors and interactions, then refine them in code. - Capture traces on first retry (
trace: "on-first-retry") to debug failures without re-running. - Pin visual regression baselines per platform and update them intentionally.
Anti-Patterns
- Avoid
page.waitForTimeout(ms)as a synchronization mechanism; rely on locator assertions and auto-wait instead. - Do not use fragile CSS selectors like
.btn-primary > span:nth-child(2); they break on minor markup changes. - Do not share mutable state between tests. Each test must be independently runnable.
- Avoid testing third-party services (payment gateways, OAuth providers) end-to-end; mock them at the network layer.
- Do not skip cross-browser testing in CI; bugs that only appear in WebKit or Firefox will reach production.
- Avoid storing authentication credentials in test files; use environment variables or secret management.
Install this skill directly: skilldb add testing-services-skills
Related Skills
Cypress
"Cypress: end-to-end/component testing, commands, intercept (network mocking), fixtures, custom commands, CI, retries"
K6
k6: load testing, performance testing, stress testing, soak testing, thresholds, checks, scenarios, browser testing, CI integration
MSW
"MSW (Mock Service Worker): API mocking, request handlers, rest/graphql, browser/node, setupServer, network-level interception"
Pact
Pact: consumer-driven contract testing, provider verification, pact broker, API compatibility, microservice integration testing
Storybook Test
Storybook Test: component testing via play functions, interaction testing, accessibility checks, visual regression, test-runner, portable stories
Testing Library
"Testing Library: React/DOM testing, queries (getBy/findBy/queryBy), user events, screen, waitFor, render, accessibility-first"