Skip to content
Learni
View all tutorials
Tests Automatisés

How to Automate E2E Tests with Playwright in 2026

Lire en français

Introduction

In 2026, Playwright stands out as the go-to tool for end-to-end (E2E) testing thanks to its rock-solid stability across Chrome, Firefox, and WebKit, smart auto-waiting, and native support for React, Vue, and Angular components. Unlike Selenium, it eliminates flakiness with powerful async APIs and built-in parallel execution.

This intermediate tutorial walks you through building a robust testing project step by step: from installation to CI/CD integration. You'll master advanced locators, network interception, and custom fixtures. By the end, your tests will be scalable, maintainable, and production-ready. Ideal for devs aiming to slash bugs by 40% on average, as shown in Microsoft benchmarks (Playwright's creators). Ready to turn manual tests into reliable automation? (142 words)

Prerequisites

  • Node.js 20+ installed
  • Intermediate TypeScript/JavaScript knowledge
  • An editor like VS Code with the Playwright extension
  • Git for version control

Initialize the Project and Install Playwright

terminal
mkdir playwright-tests && cd playwright-tests
npm init -y
npm install -D @playwright/test
npx playwright install
npx playwright test --project=chromium

These commands set up an npm project, install Playwright as a dev dependency, and download the browser binaries. The final run checks the installation by launching an example test on Chromium. Avoid npm install playwright without -D to keep prod deps clean.

Understanding the Basic Structure

Playwright creates a standard folder structure: tests/ for spec files, playwright.config.ts for global config. Each test follows the AAA pattern (Arrange-Act-Assert) with test.describe for grouping. Use page.goto() to load a page, like the demo TodoMVC app.

Configure Playwright with TypeScript

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'https://demo.playwright.dev/todomvc',
    trace: 'on-first-retry',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],

  webServer: {
    command: 'npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

This config enables parallel execution, CI retries, traces for debugging, and testing across three browsers. baseURL speeds up goto(). Add webServer for local apps. Pitfall: Forget forbidOnly in CI to avoid exclusive tests blocking runs.

Write a Simple TodoMVC Test

tests/todo.spec.ts
import { test, expect } from '@playwright/test';

test('should add two todos', async ({ page }) => {
  await page.goto('/');
  await page.getByPlaceholder('What needs to be done?').fill('buy some cheese');
  await page.getByPlaceholder('What needs to be done?').press('Enter');
  await page.getByPlaceholder('What needs to be done?').fill('feed the cat');
  await page.getByPlaceholder('What needs to be done?').press('Enter');

  await expect(page.getByText('buy some cheese')).toBeVisible();
  await expect(page.getByText('feed the cat')).toBeVisible();
});

This test adds two todos and checks their visibility. getByPlaceholder is a reliable role-based locator. Playwright auto-waits for actions. Run with npx playwright test. Avoid page.click() without hover for dropdowns.

Advanced Locators and Assertions

Powerful locators: Prefer getByRole, getByText, or chained locator() for robustness. Assertions like expect(locator).toHaveCount(n) handle auto-timeouts. Analogy: Like a GPS rerouting around blocks, Playwright retries queries automatically.

Test with Advanced Locators and Assertions

tests/advanced.spec.ts
import { test, expect } from '@playwright/test';

test('should toggle and mark todos complete', async ({ page }) => {
  await page.goto('/');

  await page.getByPlaceholder('What needs to be done?').fill('test todo');
  await page.getByPlaceholder('What needs to be done?').press('Enter');

  await page.getByLabel('Toggle todo').first().check();
  await expect(page.locator('.todo-list li.completed')).toHaveCount(1);

  await page.getByLabel('Toggle todo').first().uncheck();
  await expect(page.locator('.todo-list li:not(.completed)')).toContainText('test todo');
});

Here, getByLabel targets checkboxes, .completed filters via CSS. toHaveCount and toContainText assert dynamically. Perfect for changing states. Pitfall: Use first() or nth() for ambiguities, not fragile XPath.

Intercept Network Requests to Mock an API

tests/network.spec.ts
import { test, expect } from '@playwright/test';

test('should mock API response', async ({ page }) => {
  await page.route('**/api/users', route => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([{ id: 1, name: 'Mock User' }]),
    });
  });

  await page.goto('https://zero.webapp.io/todos');
  await expect(page.getByText('Mock User')).toBeVisible();
});

page.route intercepts fetches and mocks responses to isolate the frontend. Great for backend-free tests. fulfill simulates success/errors. Note: Use ** glob patterns for wildcards; test offline to validate.

Custom Fixtures for Reusability

Fixtures extend test.extend() for common setups like login. Save 50% on repetitive code.

Create a Login Fixture

tests/auth.spec.ts
import { test as base, expect } from '@playwright/test';

type MyFixtures = {
  loggedUser: { username: string };
};

const test = base.extend<MyFixtures>({
  loggedUser: async ({ page }, use) => {
    await page.goto('https://zero.webapp.io/login');
    await page.getByLabel('Username').fill('user');
    await page.getByLabel('Password').fill('pass');
    await page.getByRole('button', { name: 'Log in' }).click();
    await expect(page.getByText('Welcome')).toBeVisible();
    await use({ username: 'user' });
  },
});

test('should create todo as logged user', async ({ page, loggedUser }) => {
  await page.goto('https://zero.webapp.io/todos');
  await page.getByPlaceholder('What needs to be done?').fill(`Hello ${loggedUser.username}`);
  await page.getByPlaceholder('What needs to be done?').press('Enter');
});

The loggedUser fixture sets up an auth context for reuse. use() scopes the state. Scales for multi-user tests. Pitfall: Skipping await use() causes session leaks.

Integrate with CI Using GitHub Actions

.github/workflows/playwright.yml
name: Playwright Tests
on: [push, pull_request]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 20
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      run: npx playwright test
    - uses: actions/upload-artifact@v4
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

This CI workflow installs deps and browsers, then runs tests on push/PR. Uploads reports for debugging. --with-deps handles Ubuntu system libs. Optimize with matrix for multi-browser runs.

Best Practices

  • Always use role-based locators (getByRole) for UI change resilience.
  • Enable traces (trace: 'on') and videos for visual debugging.
  • Parallelize with fullyParallel: true and limit workers in CI.
  • Mock everything: APIs, storage for isolation.
  • Page Object Model for large suites: Encapsulate locators in classes.

Common Errors to Avoid

  • Flakiness: Skip sleep(); rely on expect polling.
  • Timeouts: Bump expect timeout only for slow networks, not by default.
  • Missing browsers: Always run npx playwright install before tests.
  • Polluted state: Use test.beforeEach for resets, not globals.

Next Steps

How to Automate E2E Tests with Playwright 2026 | Learni