Skip to content
Playwright Browser Testing — End-to-End Testing Guide

Playwright Browser Testing — End-to-End Testing Guide

DodaTech Updated Jun 7, 2026 7 min read

Playwright is a browser automation library by Microsoft that enables reliable end-to-end testing across Chromium, Firefox, and WebKit with a single API — no browser drivers needed.

What You’ll Learn

By the end of this tutorial, you’ll be able to set up Playwright, write robust browser tests using locators and assertions, leverage test isolation for parallel execution, integrate with CI pipelines, and debug failing tests with traces and screenshots.

Why Playwright Matters

Playwright has become the go-to choice for cross-browser testing. Its auto-waiting mechanism eliminates flaky tests caused by timing issues — the most common frustration in E2E testing. At DodaTech, Playwright validates UI flows across all three browser engines for Doda Browser before every release.

Playwright Learning Path

    flowchart LR
  A[Testing Basics] --> B[Jest]
  B --> C[Playwright]
  C --> D[Cypress]
  C --> E{You Are Here}
  style E fill:#f90,color:#fff
  
Prerequisites: Working knowledge of JavaScript. Familiarity with basic testing concepts from the Testing Basics tutorial is recommended.

Setting Up Playwright

Install Playwright with the test runner and required browsers:

npm init playwright@latest

Or add it to an existing project:

npm install --save-dev @playwright/test
npx playwright install

This downloads Chromium, Firefox, and WebKit binaries. The configuration file playwright.config.ts lets you define test directories, browser projects, and global settings.

// playwright.config.js
const { defineConfig } = require('@playwright/test');

module.exports = defineConfig({
  testDir: './tests',
  timeout: 30000,
  expect: { timeout: 5000 },
  fullyParallel: true,
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
    { name: 'webkit', use: { browserName: 'webkit' } },
  ],
});

Locators and Assertions

Playwright’s locator API uses aria roles, text content, CSS selectors, and test IDs to find elements reliably.

// login.spec.js
const { test, expect } = require('@playwright/test');

test('user can log in', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('secret123');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  await expect(page.getByTestId('user-name')).toHaveText('Welcome, User!');
});

Expected output:

Running 1 test using Chromium...
  ✓ user can log in (2.1s)

Playwright auto-waits for elements to be visible and actionable before clicking or typing — you rarely need explicit waitFor calls.

Test Isolation

Playwright creates a new browser context (isolated storage, cookies, and sessions) for every test by default.

test.describe('shopping cart', () => {
  test('adds item to cart', async ({ page }) => {
    await page.goto('/products');
    await page.getByText('Add to Cart').first().click();
    await expect(page.getByTestId('cart-count')).toHaveText('1');
  });

  test('starts with empty cart in a fresh context', async ({ page }) => {
    await page.goto('/cart');
    await expect(page.getByTestId('cart-count')).toHaveText('0');
  });
});

Each test in this block starts with a clean session. No state leaks between them, which means tests can run in parallel without flakiness.

Mocking Network Requests

Playwright lets you intercept and mock API responses to test edge cases without a real backend.

// api-test.spec.js
test('displays error when API fails', async ({ page }) => {
  await page.route('**/api/products', async route => {
    await route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Server error' }),
    });
  });

  await page.goto('/products');
  await expect(page.getByText('Failed to load products')).toBeVisible();
});

test('shows empty state when API returns no results', async ({ page }) => {
  await page.route('**/api/products', async route => {
    await route.fulfill({
      contentType: 'application/json',
      body: JSON.stringify([]),
    });
  });

  await page.goto('/products');
  await expect(page.getByText('No products found')).toBeVisible();
});

The Playwright Test Flow

    flowchart LR
  A[Write Test] --> B[Run npx playwright test]
  B --> C{Pass?}
  C -->|Yes| D[All Browsers Pass]
  C -->|No| E[View Trace Viewer]
  E --> F[Fix Test or Code]
  F --> B
  

CI Integration

Playwright integrates seamlessly with CI providers. For GitHub Actions:

name: E2E Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

This runs all tests across Chromium, Firefox, and WebKit. Failed tests produce trace files and screenshots for debugging.

Debugging with Trace Viewer

When a test fails, Playwright captures a trace — a recording of every action, network request, and console message:

npx playwright show-report

The trace viewer shows DOM snapshots at each step, network timing, and console output so you can see exactly what happened before the failure.

Common Playwright Mistakes

1. Using Generic Locators

Selectors like page.locator('div') or page.locator('button') match too many elements and produce flaky results.

Fix: Use getByRole, getByLabel, getByTestId, or getByText for specific, resilient locators.

2. Not Using Auto-Waiting

Calling page.waitForTimeout(2000) to wait for an element introduces unnecessary delays and still doesn’t guarantee the element is ready.

Fix: Rely on Playwright’s built-in auto-waiting. Use toBeVisible, toBeEnabled, and similar assertions.

3. Skipping Cross-Browser Testing

Writing tests that only run on Chromium misses bugs specific to Firefox or WebKit.

Fix: Configure all three browser projects in playwright.config.ts and run the full matrix before merging.

4. Forgetting to Handle Asynchronous Navigation

After clicking a link that triggers navigation, trying to assert immediately can hit the old page.

Fix: Use await page.waitForURL('**/new-page') or assert on the new page’s content, which automatically waits for navigation.

5. Hardcoding Test Data

Tests that depend on specific database records fail when the data changes or is cleaned up.

Fix: Use page.route to mock API responses, or create test data programmatically via API calls in beforeEach.

6. Not Using Trace on Failure

Without traces, debugging a CI failure requires guesswork.

Fix: Set trace: 'on-first-retry' or trace: 'retain-on-failure' in the config.

7. Running Tests Against Production

Running E2E tests against production risks corrupting real data and triggering unintended actions.

Fix: Run against a dedicated staging or CI environment.

Practice Questions

1. How does Playwright’s auto-waiting differ from manual waits?

Playwright automatically waits for elements to be visible, enabled, and stable before performing actions. Manual waitForTimeout fixes are avoidable.

2. What is a browser context in Playwright?

An isolated browser session similar to an incognito window. Each test gets a fresh context with separate cookies, localStorage, and session data.

3. How do you mock a network response in Playwright?

Use page.route(pattern, handler) to intercept matching requests and call route.fulfill() with the desired status, headers, and body.

4. How do you run tests in multiple browsers?

Define projects in playwright.config.ts for each browser. Playwright runs all project configurations in parallel.

5. Challenge: Write a test for a search feature.

Navigate to a site with a search box, type a query, verify results appear, mock an empty search response, and verify the empty state message.

Mini Project: E2E Test Suite for a Todo App

// todo.spec.js
const { test, expect } = require('@playwright/test');

const TODO_ITEMS = [
  'Buy groceries',
  'Walk the dog',
  'Write tests',
];

test.describe('Todo App', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('http://localhost:3000/todos');
  });

  test('shows empty state', async ({ page }) => {
    await expect(page.getByText('No todos yet')).toBeVisible();
    await expect(page.getByRole('listitem')).toHaveCount(0);
  });

  test('adds a new todo', async ({ page }) => {
    await page.getByPlaceholder('What needs to be done?').fill(TODO_ITEMS[0]);
    await page.getByRole('button', { name: 'Add' }).click();
    await expect(page.getByRole('listitem')).toHaveCount(1);
    await expect(page.getByText(TODO_ITEMS[0])).toBeVisible();
  });

  test('completes a todo', async ({ page }) => {
    await page.getByPlaceholder('What needs to be done?').fill(TODO_ITEMS[0]);
    await page.getByRole('button', { name: 'Add' }).click();
    await page.getByRole('checkbox').check();
    await expect(page.getByRole('listitem')).toHaveClass(/completed/);
  });

  test('persists between page reloads', async ({ page }) => {
    await page.getByPlaceholder('What needs to be done?').fill(TODO_ITEMS[1]);
    await page.getByRole('button', { name: 'Add' }).click();
    await page.reload();
    await expect(page.getByRole('listitem')).toHaveCount(1);
  });

  test('clears completed todos', async ({ page }) => {
    await page.getByPlaceholder('What needs to be done?').fill(TODO_ITEMS[2]);
    await page.getByRole('button', { name: 'Add' }).click();
    await page.getByRole('checkbox').check();
    await page.getByRole('button', { name: 'Clear completed' }).click();
    await expect(page.getByRole('listitem')).toHaveCount(0);
  });
});

FAQ

How is Playwright different from Selenium?
Playwright runs natively on the browser’s debugging protocol — no WebDriver needed. It supports network interception, auto-waiting, and cross-browser tests with one API. Selenium is older, slower, and requires browser-specific drivers.
Can Playwright test mobile browsers?
Yes. Playwright can emulate mobile devices (iPhone, Pixel) through device descriptors that set viewport, user-agent, and touch events.
Does Playwright support component testing?
Yes, Playwright has experimental component testing for React, Vue, and Svelte that renders components in a real browser without a full app.
How do I handle authentication in Playwright?
Use storageState to save and restore authenticated sessions. Log in once in globalSetup, save the storage state, and load it in each test worker.
What reporting options does Playwright have?
Built-in: HTML report, list, line, JSON, and JUnit (for CI). Custom reporters are supported via the reporter API.

Try It Yourself

  1. Set up a new Playwright project with npm init playwright@latest
  2. Write a test that navigates to a public website and asserts the page title
  3. Mock an API response and verify the UI handles the mocked data
  4. Run the test across Chromium, Firefox, and WebKit
  5. View the HTML report with npx playwright show-report

What’s Next

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro