Getting Started with Playwright: From Installation to Your First E2E Test

Shunku

End-to-end (E2E) testing simulates real user interactions to verify that your entire application works as expected. Playwright is an open-source E2E testing framework developed by Microsoft that lets you automate Chromium, Firefox, and WebKit with a single API.

This article walks you through setting up Playwright from scratch, writing your first test, and understanding the core concepts you need to be productive.

Why Playwright?

Among the E2E testing frameworks available today, Playwright stands out for several reasons.

flowchart LR
    subgraph PW["Playwright Strengths"]
        A["Cross-Browser"]
        B["Auto-Wait"]
        C["Parallelism"]
        D["Test Generation"]
        E["Trace Viewer"]
    end
    style PW fill:#3b82f6,color:#fff
Feature Description
Cross-browser Supports Chromium, Firefox, and WebKit (Safari)
Auto-wait Automatically waits for elements to be actionable, reducing flaky tests
Parallelism Run tests concurrently without any external service
Test generation Record browser interactions and generate test code with codegen
Multi-language Available in JavaScript/TypeScript, Python, .NET, and Java

Setup

Creating a New Project

The quickest way to start is using the initialization command.

# Create a new directory
mkdir my-e2e-tests
cd my-e2e-tests

# Initialize a Playwright project
npm init playwright@latest

The setup wizard will ask you to choose:

  • TypeScript or JavaScript
  • Test directory name (default: tests)
  • Whether to add a GitHub Actions workflow
  • Whether to install browsers

Adding to an Existing Project

If you already have a Node.js project, install the package directly.

# Install the test framework
npm install --save-dev @playwright/test

# Install browsers
npx playwright install

Generated File Structure

After initialization, you'll have the following files.

my-e2e-tests/
β”œβ”€β”€ tests/
β”‚   └── example.spec.ts      # Sample test
β”œβ”€β”€ tests-examples/
β”‚   └── demo-todo-app.spec.ts # Detailed example
β”œβ”€β”€ playwright.config.ts       # Configuration
β”œβ”€β”€ package.json
└── package-lock.json

Configuration Basics

playwright.config.ts controls how your tests run. Start with a minimal configuration.

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

export default defineConfig({
  // Directory containing test files
  testDir: './tests',

  // Test timeout in milliseconds
  timeout: 30000,

  // Enable parallel execution
  fullyParallel: true,

  // Retry on CI
  retries: process.env.CI ? 2 : 0,

  // Target browsers
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});
Option Purpose
testDir Directory where test files are located
timeout Maximum time per test
fullyParallel Allow parallel execution within files
retries Number of retry attempts on failure
projects Browser configurations to test against

Writing Your First Test

Basic Test Structure

Create a test file at tests/login.spec.ts.

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

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

  // Verify the title contains "Playwright"
  await expect(page).toHaveTitle(/Playwright/);
});

Key takeaways:

  • test defines a test case
  • { page } is a fixture automatically provided by Playwright (a browser page object)
  • All operations use async/await
  • expect verifies results

Running Tests

# Run all tests
npx playwright test

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

# Run in a specific browser
npx playwright test --project=chromium

# Run with browser visible (useful for debugging)
npx playwright test --headed

Viewing the Test Report

After tests finish, view the HTML report.

npx playwright show-report

Locators: Finding Elements

Locators identify elements on the page. Playwright recommends accessibility-based locators that reflect how real users perceive the page.

Recommended Locators (getBy Series)

// By role (WAI-ARIA) β€” most recommended
await page.getByRole('button', { name: 'Submit' });
await page.getByRole('heading', { name: 'Welcome' });
await page.getByRole('link', { name: 'Sign in' });

// By label (ideal for form elements)
await page.getByLabel('Email address');

// By placeholder
await page.getByPlaceholder('Search...');

// By text content
await page.getByText('I agree to the terms');

// By test ID (custom attribute)
await page.getByTestId('submit-button');

Locator Priority

flowchart TD
    A["getByRole"] --> B["getByLabel"]
    B --> C["getByPlaceholder"]
    C --> D["getByText"]
    D --> E["getByTestId"]
    E --> F["CSS Selectors"]

    style A fill:#22c55e,color:#fff
    style B fill:#22c55e,color:#fff
    style C fill:#3b82f6,color:#fff
    style D fill:#3b82f6,color:#fff
    style E fill:#f59e0b,color:#fff
    style F fill:#ef4444,color:#fff
Priority Locator Reason
High getByRole Follows accessibility standards, closest to how users perceive elements
High getByLabel Best for form elements
Medium getByPlaceholder Fallback when labels are absent
Medium getByText When visible text uniquely identifies an element
Low getByTestId Safe fallback when other locators don't work
Avoid CSS selectors Fragile against UI changes

getByRole is preferred because it matches how assistive technologies like screen readers identify elements. Your tests double as accessibility checks.

Assertions: Verifying Results

Auto-Retrying Assertions

Playwright assertions automatically retry until the condition is met. This is key to preventing flaky tests.

// Visibility
await expect(page.getByRole('alert')).toBeVisible();
await expect(page.getByRole('alert')).toBeHidden();

// Text content
await expect(page.getByRole('heading')).toHaveText('Dashboard');
await expect(page.locator('.message')).toContainText('Success');

// URL
await expect(page).toHaveURL(/dashboard/);

// Element count
await expect(page.getByRole('listitem')).toHaveCount(3);

// Input value
await expect(page.getByLabel('Name')).toHaveValue('John Doe');

// CSS class
await expect(page.locator('.btn')).toHaveClass(/active/);

Soft Assertions

Regular assertions stop the test on failure. Soft assertions run all checks and report failures together.

await expect.soft(page.getByTestId('status')).toHaveText('OK');
await expect.soft(page.getByTestId('count')).toHaveText('5');
// Both results appear in the report

Practical Test Example

Here's a complete login form test.

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

test.describe('Login', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('logs in with valid credentials', async ({ page }) => {
    await page.getByLabel('Email').fill('user@example.com');
    await page.getByLabel('Password').fill('password123');
    await page.getByRole('button', { name: 'Sign in' }).click();

    await expect(page).toHaveURL(/dashboard/);
    await expect(page.getByRole('heading')).toHaveText('Dashboard');
  });

  test('shows error on empty form submission', async ({ page }) => {
    await page.getByRole('button', { name: 'Sign in' }).click();

    await expect(page.getByRole('alert')).toBeVisible();
    await expect(page.getByRole('alert')).toContainText('required');
  });

  test('shows error on wrong password', async ({ page }) => {
    await page.getByLabel('Email').fill('user@example.com');
    await page.getByLabel('Password').fill('wrong');
    await page.getByRole('button', { name: 'Sign in' }).click();

    await expect(page.getByRole('alert')).toContainText('Authentication failed');
  });
});

Notable patterns:

  • test.describe groups related tests
  • test.beforeEach navigates to the login page before each test
  • getByLabel and getByRole find elements accessibly
  • No explicit waits (waitFor or sleep) needed

Debugging

When tests fail, Playwright provides powerful debugging tools.

UI Mode

An interactive UI for watching tests run in real time.

npx playwright test --ui

Debug Mode

Step through each action in your test.

npx playwright test --debug

Trace Viewer

Investigate CI failures after the fact. Enable it in your config.

// playwright.config.ts
export default defineConfig({
  use: {
    trace: 'on-first-retry', // Record trace on retry
  },
});

Traces capture:

  • Screenshots at each step
  • DOM snapshots
  • Network requests
  • Console logs
# Open a trace file
npx playwright show-trace trace.zip

Test Generation (codegen)

Generate test code by interacting with a browser.

npx playwright codegen https://example.com

A browser opens and records your actions as Playwright code. It's also a great way for beginners to learn how locators work.

How Auto-Wait Works

Auto-wait is what sets Playwright apart from many other frameworks.

flowchart TD
    A["Execute action<br/>e.g. click()"] --> B{"Element<br/>exists?"}
    B -->|No| C["Retry"]
    C --> B
    B -->|Yes| D{"Element<br/>visible?"}
    D -->|No| C
    D -->|Yes| E{"Element<br/>stable?"}
    E -->|No| C
    E -->|Yes| F{"Element<br/>enabled?"}
    F -->|No| C
    F -->|Yes| G{"Not obscured<br/>by overlay?"}
    G -->|No| C
    G -->|Yes| H["Execute action"]

    style A fill:#3b82f6,color:#fff
    style H fill:#22c55e,color:#fff
    style C fill:#f59e0b,color:#fff

When you call click(), Playwright automatically waits until the element is ready to be clicked. This eliminates fragile patterns like:

// Bad: manual wait
await page.waitForTimeout(3000);
await page.click('#submit');

// Good: let Playwright handle it
await page.getByRole('button', { name: 'Submit' }).click();
// Automatically waits until clickable

Summary

Concept Key Point
Setup npm init playwright@latest gets you started instantly
Locators Prefer getByRole above all others
Assertions Auto-retry eliminates the need for sleep
Auto-wait Automatically checks element actionability
Debugging UI mode, Trace Viewer, and codegen are all available
Configuration playwright.config.ts controls browsers, parallelism, and retries

Playwright's greatest advantage is keeping test code simple. Auto-wait and accessible locators let you focus on what the user does rather than when elements appear.

References