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.
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')
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()orwaitForTimeout() - Use getByRole as your default locator strategy
- Run with
--uiwhile 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.