Skip to content

Playwright Reference: Locators, Actions, Page Objects, API Mocking & CI

Playwright is Microsoft’s E2E testing framework supporting Chromium, Firefox, and WebKit in a single API. The key advantage over Cypress: true multi-browser support, parallel execution out of the box, and a network interception API that actually works at the browser level. The most important concept to get right early: Playwright uses auto-waiting — you never need sleep() or explicit waits. Every action (click, fill, etc.) automatically waits for the element to be visible, stable, and enabled.

1. Setup, Config & First Test

Install, playwright.config.ts, browsers, base URL, and test structure
# npm init playwright@latest
# npm install -D @playwright/test
# npx playwright install   # downloads browsers (~300MB)

// playwright.config.ts:
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./e2e",
  fullyParallel: true,           // all tests run in parallel by default
  retries: process.env.CI ? 2 : 0,  // retry on CI, not locally
  workers: process.env.CI ? 1 : undefined,
  reporter: [["html"], ["list"]],
  use: {
    baseURL: "http://localhost:3000",
    trace: "on-first-retry",     // capture trace on retry for debugging
    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,
  },
});

// e2e/login.spec.ts:
import { test, expect } from "@playwright/test";

test("user can log in", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill("alice@example.com");
  await page.getByLabel("Password").fill("secret123");
  await page.getByRole("button", { name: "Sign in" }).click();
  await expect(page).toHaveURL("/dashboard");
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
});

2. Locators — The Right Way to Find Elements

Role, label, text, test-id locators — and why to avoid CSS selectors
// Prefer user-visible locators over CSS/XPath — they survive refactors:

// By role (ARIA role — most resilient):
page.getByRole("button", { name: "Submit" })
page.getByRole("textbox", { name: "Email" })
page.getByRole("link", { name: "Home" })
page.getByRole("heading", { level: 1 })
page.getByRole("checkbox", { name: "Remember me" })
page.getByRole("listitem")    // all li elements
page.getByRole("dialog")

// By label (form inputs):
page.getByLabel("Email address")    // finds input with associated label
page.getByLabel("Password")

// By placeholder:
page.getByPlaceholder("Search...")

// By text (for non-interactive elements):
page.getByText("Welcome back, Alice")
page.getByText(/error/i)   // regex

// By alt text (images):
page.getByAltText("Company logo")

// By test ID (for complex cases where other locators don't work):
// Add data-testid="submit-btn" to HTML → page.getByTestId("submit-btn")

// Chaining — scope to a parent:
const loginForm = page.getByRole("form", { name: "Login" });
loginForm.getByLabel("Email").fill("alice@example.com");

// Filtering:
page.getByRole("listitem").filter({ hasText: "Alice" })
page.getByRole("listitem").nth(0)   // first item
page.getByRole("listitem").last()

// Avoid (fragile): page.locator(".btn-primary"), page.locator("//button[1]")

3. Actions, Assertions & Auto-Waiting

Click, fill, select, upload, keyboard — and expect assertions with custom timeouts
// Actions (all auto-wait for element to be visible + stable + enabled):
await page.getByLabel("Email").fill("alice@example.com");   // clears then types
await page.getByLabel("Email").type("alice@example.com");   // types char by char (for autocomplete)
await page.getByRole("button", { name: "Submit" }).click();
await page.getByRole("button", { name: "Submit" }).dblclick();
await page.getByRole("checkbox").check();
await page.getByRole("checkbox").uncheck();
await page.getByRole("combobox").selectOption("London");
await page.getByRole("combobox").selectOption({ label: "London" });

// File upload:
await page.getByLabel("Upload file").setInputFiles("path/to/file.pdf");

// Keyboard:
await page.keyboard.press("Tab");
await page.keyboard.press("Enter");
await page.keyboard.press("Control+A");

// Assertions (expect auto-waits up to timeout — default 5s):
await expect(page).toHaveURL("/dashboard");
await expect(page).toHaveTitle(/My App/);
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
await expect(locator).toHaveText("Hello");
await expect(locator).toContainText("Hello");
await expect(locator).toHaveValue("alice@example.com");  // input value
await expect(locator).toHaveCount(5);    // number of matching elements

// Custom timeout:
await expect(locator).toBeVisible({ timeout: 10_000 });

// Soft assertions — don't stop test on failure:
await expect.soft(locator).toBeVisible();
await expect.soft(locator).toHaveText("Hello");
// All soft assertions are reported together at the end

4. Page Object Model & Fixtures

Encapsulate pages in classes, share state via fixtures, and bypass authentication
// Page Object Model — encapsulate page interactions:
// e2e/pages/LoginPage.ts
import { type Page, type Locator } from "@playwright/test";

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput    = page.getByLabel("Email");
    this.passwordInput = page.getByLabel("Password");
    this.submitButton  = page.getByRole("button", { name: "Sign in" });
  }

  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();
    await this.page.waitForURL("/dashboard");
  }
}

// Custom fixtures — share authenticated state:
// e2e/fixtures.ts
import { test as base } from "@playwright/test";
import { LoginPage } from "./pages/LoginPage";

type Fixtures = { loggedInPage: Page };

export const test = base.extend<Fixtures>({
  loggedInPage: async ({ page }, use) => {
    const login = new LoginPage(page);
    await login.goto();
    await login.login("alice@example.com", "secret123");
    await use(page);   // yield authenticated page to test
  },
});

// Use in tests:
import { test, expect } from "./fixtures";
test("authenticated user sees dashboard", async ({ loggedInPage }) => {
  await expect(loggedInPage.getByRole("heading", { name: "Dashboard" })).toBeVisible();
});

5. API Mocking, Tracing & CI

Intercept network requests, route mocking, HAR recording, and GitHub Actions setup
// Network interception — mock API responses:
await page.route("/api/users", (route) => {
  route.fulfill({
    status: 200,
    contentType: "application/json",
    body: JSON.stringify([{ id: 1, name: "Alice" }]),
  });
});

// Intercept + modify (proxy-style):
await page.route("/api/**", async (route) => {
  const response = await route.fetch();
  const json = await response.json();
  json.injected = true;
  await route.fulfill({ response, json });
});

// Block specific resources (speed up tests):
await page.route("**/*.{png,jpg,gif,svg}", route => route.abort());
await page.route("**google-analytics**", route => route.abort());

// Assert request was made:
const responsePromise = page.waitForResponse("/api/users");
await page.getByRole("button", { name: "Load Users" }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);

// Tracing — record + replay test execution:
// playwright.config.ts: trace: "on-first-retry"
// View trace: npx playwright show-trace trace.zip

// GitHub Actions:
// - uses: microsoft/playwright-github-action@v1
// Or manually:
// - run: npx playwright install --with-deps chromium
// - run: npx playwright test
// - uses: actions/upload-artifact@v4
//   if: always()
//   with:
//     name: playwright-report
//     path: playwright-report/

// Headed mode (see browser):
// npx playwright test --headed
// npx playwright test --debug    # step through with DevTools
// npx playwright codegen http://localhost:3000   # record interactions to code

Track Node.js and testing toolchain releases at ReleaseRun. Related: Vitest Reference | Next.js App Router Reference | TypeScript Reference

🔍 Free tool: npm Package Health Checker — check Playwright and test utility packages for known CVEs and latest version before CI upgrades.

Founded

2023 in London, UK

Contact

hello@releaserun.com