Getting Started with Playwright: A Practical Guide to End-to-End Testing
On this page
Getting Started with Playwright: A Practical Guide to End-to-End Testing
I'll admit it — I resisted end-to-end testing for years. Selenium was painful, Cypress had its own quirks, and the whole experience felt like fighting the tools more than testing the app. Playwright changed that for me. Microsoft built it from scratch to avoid the mistakes of previous tools, and it shows. Tests run fast, they're rarely flaky out of the box, and the developer tooling is genuinely excellent.
Here's everything you need to go from zero to a working test suite.
Installation and Setup
Getting started is one command:
npm init playwright@latest
This scaffolds everything for you — a playwright.config.ts, an example test, a tests directory, and it downloads the browser binaries. If you'd rather add it to an existing project manually:
npm install -D @playwright/test
npx playwright install
The playwright install step grabs the browser binaries. You only need to do this once, or again after updating Playwright.
Configuring Playwright
The playwright.config.ts file is where the magic happens. Here's what I actually use as a starting config:
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: process.env.BASE_URL || 'http://localhost:3000',
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 dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
A few things worth pointing out: fullyParallel runs everything concurrently — tests are fast. trace: 'on-first-retry' captures a trace when a test fails and gets retried, which is invaluable when you're trying to figure out why something failed in CI at 11 PM. And webServer automatically spins up your dev server before tests run, so you don't have to remember to start it separately.
Writing Your First Test
Create a file at tests/home.spec.ts:
import { test, expect } from '@playwright/test';
test('homepage has the correct title', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/My App/);
});
test('navigation links work', async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: 'About' }).click();
await expect(page).toHaveURL(/\/about/);
await expect(page.getByRole('heading', { name: 'About Us' })).toBeVisible();
});
Two things I love here. First, the expect assertions auto-wait for conditions to be true. No manual waits, no sleep statements, no waitFor calls. Second, getByRole finds elements the way a screen reader would — which means your tests are resilient to markup changes AND you're accidentally validating accessibility. Win-win.
Running Tests
Run everything:
npx playwright test
For day-to-day work, these flags are my most-used:
# Run a specific test file
npx playwright test tests/home.spec.ts
# Run in headed mode (see the browser)
npx playwright test --headed
# Run in UI mode (interactive test explorer)
npx playwright test --ui
# Debug a specific test
npx playwright test tests/home.spec.ts --debug
UI mode is a game-changer when you're writing new tests. It shows a live browser, a timeline of every action, and DOM snapshots at each step. I wish I'd had this tool years ago.
Integrating with CI
Playwright plays nicely with any CI environment. Here's what a GitHub Actions workflow looks like:
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
The --with-deps flag installs system-level dependencies the browsers need on Linux — without it, you'll get cryptic errors about missing shared libraries. The report upload preserves the HTML report as a build artifact so you can dig into failures after the fact.
Best Practices
Focus on critical user flows. E2E tests are the most expensive to run and maintain. Cover the paths that actually matter — signup, checkout, core features — and leave edge cases to unit and integration tests. I've seen teams write 500 E2E tests and spend more time maintaining them than writing features.
Group related tests with test.describe and share setup logic. Use test names that describe expected behavior, not implementation details.
FAQ
How do I test an app that requires authentication?
Use Playwright's storageState feature. Write a setup step that logs in and saves the browser state to a file, then load that state in tests that need an authenticated user. This avoids repeating the login flow in every single test — which is both slow and brittle.
Can I use Playwright with Next.js, Vite, or Remix?
Absolutely. The webServer config works with any dev server command. Set command to whatever starts your app and url to where it serves. Playwright waits for the server to be ready before running tests.
How do I handle flaky tests?
First, actually diagnose the root cause. Common culprits: hardcoded timeouts (waitForTimeout is almost always a smell), race conditions with animations, and tests depending on external services. Replace timeouts with Playwright's built-in auto-waiting. Mock external APIs with page.route(). If a test is genuinely non-deterministic, use test.retry(2) on that specific test while you investigate — but don't just leave the retry there forever.
Should I use test IDs or role-based locators?
Start with role-based locators (getByRole, getByLabel, getByText). They make tests more readable and validate accessibility. Fall back to getByTestId for elements that don't have a meaningful role, like a specific container or a canvas element.
How do I run tests against staging or production?
Override the base URL:
BASE_URL=https://staging.example.com npx playwright test
Or use a separate config file for different environments.
How fast is Playwright compared to Cypress or Selenium?
Noticeably faster in my experience. Native browser control (no WebDriver overhead), parallel execution by default, and efficient browser context isolation mean a suite of 50 tests that takes minutes in other tools often runs in under a minute with Playwright.
Wrapping Up
Playwright gives you a modern, reliable foundation for E2E testing that actually works without constant babysitting. Setup takes minutes, the dev tools are excellent, and the auto-waiting behavior eliminates the most common source of flaky tests. Start with a handful of tests covering your most critical flows, run them in CI on every push, and expand from there.
Sources
- Playwright Documentation — Official getting started guide and API reference
- Playwright — Test Configuration — Configuring test projects, browsers, and parallelism
- Playwright — CI/CD Integration — Setting up Playwright in GitHub Actions, GitLab CI, and other CI platforms
- Playwright — Best Practices — Official recommendations for writing reliable end-to-end tests