Playwright E2E Testing: Ship Your First Test Today

Learn to set up Playwright and write your first end-to-end test in minutes.

Kodetra TechnologiesKodetra Technologies
8 min read
Apr 3, 2026
0 views
Playwright E2E Testing: Ship Your First Test Today

I broke production on a Friday.

Not my proudest moment. A tiny CSS change nuked the checkout button on mobile, and nobody caught it until customers started tweeting. That Monday, I set up Playwright. I haven't shipped a broken checkout since.

If you've been putting off end-to-end testing because it sounds painful, I get it. Selenium scarred a whole generation of developers. But Playwright is genuinely different β€” it's fast, it's reliable, and you can have your first real test running in about ten minutes. Let me show you how.

Why Playwright Won the Testing Wars

Before we dive into code, here's why Playwright has become the default choice for E2E testing in 2026. Built by Microsoft (by the same folks who originally built Puppeteer at Google), it solves the problems that made browser testing miserable for years.

No more flaky tests. Playwright auto-waits for elements to be ready before interacting with them. No more sleep(3000) hacks scattered through your test suite.

One API, every browser. Chromium, Firefox, and WebKit β€” you write your test once and it runs everywhere. Yes, that includes Safari rendering via WebKit.

Here's how Playwright fits into a typical dev workflow:

text

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Developer   │────▢│  Git Push    │────▢│   CI/CD     β”‚
β”‚  writes code β”‚     β”‚  to branch   β”‚     β”‚  pipeline   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                                                β”‚
                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                    β”‚   Playwright Tests    β”‚
                                    β”‚                       β”‚
                                    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                                    β”‚  β”‚ Chromium  βœ“     β”‚  β”‚
                                    β”‚  β”‚ Firefox   βœ“     β”‚  β”‚
                                    β”‚  β”‚ WebKit    βœ“     β”‚  β”‚
                                    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                β”‚
                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                    β”‚  Pass? ──▢ Deploy     β”‚
                                    β”‚  Fail? ──▢ Block PR   β”‚
                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Setting Up Playwright From Scratch

Let's get a project going. You'll need Node.js 18 or later installed. Open your terminal and run:

bash

mkdir my-playwright-tests
cd my-playwright-tests
npm init -y
npm init playwright@latest

The init wizard will ask you a few questions. Here's what I recommend:

text

βœ” Do you want to use TypeScript or JavaScript? β†’ TypeScript
βœ” Where to put your end-to-end tests? β†’ tests
βœ” Add a GitHub Actions workflow? β†’ true
βœ” Install Playwright browsers? β†’ true

Once it finishes, your project looks like this:

text

my-playwright-tests/
β”œβ”€β”€ tests/
β”‚   └── example.spec.ts
β”œβ”€β”€ tests-examples/
β”‚   └── demo-todo-app.spec.ts
β”œβ”€β”€ playwright.config.ts
β”œβ”€β”€ package.json
└── .github/
    └── workflows/
        └── playwright.yml

The scaffolding gives you a working config, an example test, and even a CI workflow. That's one of the things I love about Playwright β€” it respects your time.

Understanding the Config File

Before writing tests, let's look at playwright.config.ts. This is your command center:

typescript

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: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

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

The key settings here: fullyParallel runs your tests concurrently so they finish fast. The trace: 'on-first-retry' setting captures a full trace when a test fails, which is incredibly useful for debugging. And screenshot: 'only-on-failure' gives you visual evidence of what went wrong.

Writing Your First Real Test

Delete the example test and create tests/login.spec.ts. We'll test a login flow β€” something almost every app needs:

typescript

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

test.describe('Login Flow', () => {
  test('should log in with valid credentials', async ({ page }) => {
    // Navigate to the login page
    await page.goto('/login');

    // Fill in the form
    await page.getByLabel('Email').fill('dev@example.com');
    await page.getByLabel('Password').fill('testpass123');

    // Click the login button
    await page.getByRole('button', { name: 'Sign in' }).click();

    // Verify we landed on the dashboard
    await expect(page).toHaveURL('/dashboard');
    await expect(
      page.getByRole('heading', { name: 'Welcome back' })
    ).toBeVisible();
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.goto('/login');

    await page.getByLabel('Email').fill('wrong@example.com');
    await page.getByLabel('Password').fill('badpassword');
    await page.getByRole('button', { name: 'Sign in' }).click();

    // Check for error message
    await expect(
      page.getByText('Invalid email or password')
    ).toBeVisible();

    // Verify we stayed on the login page
    await expect(page).toHaveURL('/login');
  });
});

Notice how readable this is. No XPath selectors, no CSS gymnastics. Playwright's locator API lets you find elements the way a user would β€” by label, by role, by text. This makes your tests resilient to markup changes.

Running and Debugging Tests

Run your tests from the terminal:

bash

# Run all tests
npx playwright test

# Run a specific test file
npx playwright test tests/login.spec.ts

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

# Run in UI mode (interactive debugger)
npx playwright test --ui

The UI mode is a game changer. It gives you a visual timeline of every action, lets you step through tests, and shows you exactly what Playwright sees at each moment. When I'm writing new tests, I almost always start in UI mode.

Here's the test execution flow:

text

npx playwright test
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Read config      β”‚
β”‚  (playwright.     β”‚
β”‚   config.ts)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Launch browsers  │────▢│  Chromium     β”‚
β”‚  (parallel)       │────▢│  Firefox      β”‚
β”‚                   │────▢│  WebKit       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Execute tests    β”‚
β”‚  per browser      β”‚
β”‚  (auto-wait,      β”‚
β”‚   retry on fail)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Generate report  β”‚
β”‚  (HTML + traces)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

After a run, open the HTML report:

bash

npx playwright show-report

You get a full breakdown of pass/fail status, timing, screenshots on failure, and downloadable traces.

Testing API Calls With Route Interception

One of Playwright's superpowers is network interception. You can mock API responses without changing your app code:

typescript

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

test('should display user profile from API', async ({ page }) => {
  // Intercept the API call and return mock data
  await page.route('**/api/user/profile', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        name: 'Ada Lovelace',
        email: 'ada@example.com',
        role: 'admin',
      }),
    });
  });

  await page.goto('/profile');

  await expect(page.getByText('Ada Lovelace')).toBeVisible();
  await expect(page.getByText('admin')).toBeVisible();
});

This is incredibly useful for testing error states, loading states, and edge cases without needing a real backend running. You can simulate slow networks, server errors, and empty responses to make sure your UI handles every scenario gracefully.

Adding Playwright to Your CI Pipeline

Tests are only as good as how consistently you run them. Playwright ships with GitHub Actions support out of the box, but it works with any CI provider. Here's a minimal GitHub Actions config that runs your tests on every pull request:

yaml

name: Playwright Tests
on:
  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: 14

The upload-artifact step is the key part most folks miss. When a test fails in CI, you want the HTML report and trace files available for download. Without them, you're stuck guessing what went wrong from terminal output alone. I've seen teams waste hours debugging CI failures that would've taken two minutes with a trace file open.

If you're using GitLab CI, CircleCI, or Jenkins, the pattern is the same: install browsers with npx playwright install --with-deps, run the tests, and upload the report as an artifact. The Playwright docs have examples for every major CI provider.

One more tip: run your tests against a preview deployment rather than localhost. Most modern hosting platforms like Vercel, Netlify, and Railway generate preview URLs for each PR. Point Playwright at those URLs and you're testing the real thing, not a local approximation.

Common Mistakes and How to Avoid Them

After running Playwright across dozens of projects, here are the traps I see developers fall into.

Using page.waitForTimeout() instead of proper waits. If you're writing await page.waitForTimeout(2000), you're bringing Selenium habits into Playwright. The whole point of auto-wait is that you don't need arbitrary delays. Use expect assertions β€” they auto-retry until the condition is met or the timeout expires.

Relying on CSS selectors for everything. Playwright gives you getByRole, getByLabel, getByText, and getByTestId. Use them. They make your tests readable and resistant to refactors. I only reach for CSS selectors as a last resort.

Not using test fixtures. If every test starts with the same login flow, extract it into a fixture:

typescript

import { test as base } from '@playwright/test';

const test = base.extend({
  authenticatedPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill('dev@example.com');
    await page.getByLabel('Password').fill('testpass123');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await use(page);
  },
});

test('dashboard loads for logged-in user', async ({ authenticatedPage }) => {
  await authenticatedPage.goto('/dashboard');
  // ... your assertions
});

Ignoring the trace viewer. When a test fails in CI, the trace file is gold. It records every network request, DOM snapshot, and console log. Configure your CI to upload trace artifacts and you'll debug failures in minutes instead of hours.

Testing too much in one test. Each test should verify one behavior. Long tests with many assertions are hard to debug when they fail. Keep them focused and name them clearly so that when something goes red in CI, you know what broke without even opening the report.

Skipping mobile viewport tests. Half your users are on phones. Playwright makes it trivial to test responsive layouts by setting a viewport size in your config's projects array. Add a mobile Chrome project and you'll catch layout bugs before your users do.

Wrapping Up

Playwright has made E2E testing something I actually look forward to. The auto-waiting, the built-in debugging tools, the clean API β€” it all adds up to tests you can trust and tests you can maintain.

Start small. Pick one critical user flow in your app β€” login, checkout, signup β€” and write a Playwright test for it today. Once you see how fast you can go from zero to a passing test, you'll want to cover more.

The official Playwright docs are excellent, and the community is active and helpful. You've got everything you need to stop shipping bugs on Fridays.


Want to share what you've learned? CodeBrainery is a community where developers teach developers. If you've built something cool with Playwright, figured out a tricky testing pattern, or just want to write about what you know β€” we'd love to have you. Head over to codebrainery.com and start writing your first article today.

Found this useful? Share it with a teammate who's still writing sleep(3000) in their tests.

Kodetra Technologies

Kodetra Technologies

Kodetra Technologies is a software development company that specializes in creating custom software solutions, mobile apps, and websites that help businesses achieve their goals.

0 followers

Comments

No comments yet. Be the first to comment!