End-to-end testing used to be fragile and painful. Playwright changed that. Developed by Microsoft, it supports Chromium, Firefox, and WebKit with a single API, runs tests in parallel, and has built-in auto-waiting so you don't write sleep() calls.

This guide uses TypeScript. If you prefer JavaScript, the syntax is nearly identical — just remove the type annotations.

1. Install Playwright

# Create a new project and install Playwright
npm init playwright@latest

# Choose TypeScript, add example tests, install browsers
# Playwright will download Chromium, Firefox, and WebKit automatically

After setup you'll have a tests/ folder with example specs and a playwright.config.ts file. Run the examples to confirm everything works:

npx playwright test --ui

The --ui flag opens the Playwright UI mode — a visual test runner that lets you step through tests, inspect the DOM, and replay failures. Very useful when learning.

2. Your First Test

Create tests/homepage.spec.ts:

import { test, expect } from '@playwright/test';

test('homepage has correct title', async ({ page }) => {
  await page.goto('https://playwright.dev');

  // Assert the page title
  await expect(page).toHaveTitle(/Playwright/);
});

test('get started link works', async ({ page }) => {
  await page.goto('https://playwright.dev');

  // Click the link
  await page.getByRole('link', { name: 'Get started' }).click();

  // Expect to land on the installation page
  await expect(page).toHaveURL(/.*intro/);
});

Run it:

npx playwright test tests/homepage.spec.ts

3. Locators — Finding Elements

Playwright's locator API is its biggest strength. It avoids fragile CSS/XPath selectors and prefers role-based and text-based queries that mirror how users actually perceive the page:

// By accessible role (recommended)
page.getByRole('button', { name: 'Submit' })
page.getByRole('textbox', { name: 'Email' })

// By visible text
page.getByText('Welcome back')

// By label (for form fields)
page.getByLabel('Password')

// By placeholder text
page.getByPlaceholder('Enter your email')

// By test ID (most stable, requires data-testid on your elements)
page.getByTestId('login-button')
Prefer getByRole and getByLabel. They make tests accessible-by-default and survive UI refactors better than CSS selectors.

4. A Complete Login Test

Here's a realistic example testing a login flow:

test('user can log in', async ({ page }) => {
  await page.goto('/login');

  // Fill the form
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('secret123');

  // Submit
  await page.getByRole('button', { name: 'Log in' }).click();

  // Assert redirect to dashboard
  await expect(page).toHaveURL(/.*dashboard/);
  await expect(page.getByText('Welcome back')).toBeVisible();
});

5. Running Across Browsers

In playwright.config.ts, Playwright already configures three browser projects. Run all of them with:

# Run on all browsers
npx playwright test

# Run on Chromium only
npx playwright test --project=chromium

# Run in headed mode (see the browser)
npx playwright test --headed

Key Things to Remember

  • Playwright auto-waits for elements — you don't need manual sleep() or waitForTimeout()
  • Use getByRole as your default locator strategy
  • Run with --ui while writing tests to debug visually
  • Playwright generates a full HTML report after each run — open it with npx playwright show-report
  • Tests run in parallel by default — keep them independent

What's Next

Once you're comfortable with the basics, explore page object models for organising large test suites, fixtures for shared setup, and API request interception for mocking backend responses during UI tests.

Our Introduction to Playwright course covers all of this end-to-end, including CI/CD integration with GitHub Actions.