Skip to main content
All articles
Automation

Getting Started with Playwright: A Beginner's Guide to Browser Automation

10 min readBy Elmonds Kreslins

Why Playwright?

Playwright is Microsoft's open-source browser automation framework. It lets you write tests that control a real browser, clicking buttons, filling forms, navigating pages, and asserting that things look and behave the way they should.

It has become the standard choice for new projects for three reasons. First, it supports Chromium, Firefox, and WebKit (Safari) from a single test suite. Second, it has auto-waiting built in, meaning it waits for elements to be ready before interacting with them, which eliminates most of the flaky test problems that plagued older tools. Third, the developer experience is genuinely good: TypeScript support, excellent error messages, a visual test runner, and a codegen tool that writes tests by recording your browser actions.

This guide will get you from zero to a working test suite. We will use TypeScript throughout since that is the recommended setup, but the concepts apply equally to JavaScript.

Prerequisites

You need Node.js 18 or higher. Check your version:

node --version

If you need to install or upgrade Node.js, use the official installer or a version manager like nvm.

Installation

Create a new directory for your tests (or navigate to an existing project) and run:

npm init playwright@latest

The installer will ask a few questions:

  • TypeScript or JavaScript? Choose TypeScript.
  • Where to put your end-to-end tests? The default tests directory is fine.
  • Add a GitHub Actions workflow? Yes, even if you are not using it immediately. It is useful to have.
  • Install Playwright browsers? Yes.

This creates a playwright.config.ts file, a tests/ directory with an example test, and installs the three browser engines (Chromium, Firefox, WebKit). The browser engines are around 300MB total.

Running the example test

The installer creates a sample test at tests/example.spec.ts. Run it:

npx playwright test

Playwright will run the test in all three browsers by default and show a summary. To open the HTML report:

npx playwright show-report

To run with a visible browser window (useful when writing tests):

npx playwright test --headed

Writing your first test

Create a new file at tests/homepage.spec.ts:

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

test('homepage has correct title', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example Domain/);
});

test('heading is visible', async ({ page }) => {
  await page.goto('https://example.com');
  const heading = page.getByRole('heading', { name: 'Example Domain' });
  await expect(heading).toBeVisible();
});

Breaking this down:

  • test(description, async ({ page }) => {}) defines a test. The page object is a Playwright browser page.
  • page.goto(url) navigates to a URL and waits for the page to load.
  • expect(page).toHaveTitle() asserts the page title. Playwright waits automatically if the title is not immediately available.
  • page.getByRole() finds an element by its ARIA role. This is the preferred way to find elements because it mirrors how assistive technology and real users interact with the page.

Selectors: how to find elements

Playwright offers several ways to locate elements. Listed from most to least recommended:

By role (preferred)

page.getByRole('button', { name: 'Submit' })
page.getByRole('link', { name: 'Sign in' })
page.getByRole('textbox', { name: 'Email' })
page.getByRole('heading', { name: 'Welcome' })

Role selectors are resilient to visual changes and match how screen readers navigate the page. Use these wherever possible.

By label

page.getByLabel('Email address')
page.getByLabel('Password')

For form inputs, getByLabel finds the input associated with a given label text. This also doubles as an accessibility check: if your label is not properly associated with the input, the selector will not find it.

By placeholder

page.getByPlaceholder('Enter your email')

By text

page.getByText('Confirm your order')

By test ID (last resort)

page.getByTestId('submit-button')

This requires adding data-testid attributes to your HTML. Use it when no semantic selector is available, but prefer role and label selectors first.

CSS selectors (avoid if possible)

page.locator('.submit-button')
page.locator('#email-input')

CSS selectors are brittle. A class name or ID rename breaks the test. Use them only when you have no alternative.

Common assertions

// Page-level
await expect(page).toHaveTitle('My App');
await expect(page).toHaveURL('https://myapp.com/dashboard');

// Element visibility
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();

// Element text
await expect(locator).toHaveText('Order confirmed');
await expect(locator).toContainText('confirmed');

// Input value
await expect(locator).toHaveValue('hello@example.com');

// Disabled/enabled
await expect(locator).toBeDisabled();
await expect(locator).toBeEnabled();

// Count
await expect(page.getByRole('listitem')).toHaveCount(5);

All Playwright assertions have built-in retry logic. If the assertion fails initially, Playwright keeps retrying for up to 5 seconds (configurable) before marking the test as failed. This removes the need for manual waits in most cases.

Actions: interacting with the page

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

// Fill a text input (clears first)
await page.getByLabel('Email').fill('test@example.com');

// Type character by character (useful for autocomplete testing)
await page.getByLabel('Search').pressSequentially('playwright');

// Select a dropdown option
await page.getByLabel('Country').selectOption('GB');

// Check a checkbox
await page.getByLabel('I agree to the terms').check();

// Press a keyboard key
await page.keyboard.press('Enter');
await page.keyboard.press('Tab');

// Hover
await page.getByRole('button', { name: 'Menu' }).hover();

Writing a real login test

Here is a practical example testing a login flow:

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

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

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

  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

test('login shows error with wrong password', async ({ page }) => {
  await page.goto('/login');

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

  await expect(page.getByRole('alert')).toContainText('Invalid credentials');
  await expect(page).toHaveURL('/login');
});

Configuration

The playwright.config.ts file controls which browsers to run, base URL, timeouts, and more. A typical project config:

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  timeout: 30_000,
  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'] } },
    { name: 'mobile', use: { ...devices['iPhone 14'] } },
  ],
});

Key settings:

  • baseURL: set this so you can use relative paths (page.goto('/login')) instead of full URLs
  • retries: 2 in CI: retries flaky tests twice before marking them failed, which reduces noise from infrastructure issues
  • trace: 'on-first-retry': records a full trace (DOM snapshots, network, console) when a test is retried, making debugging much easier
  • screenshot: 'only-on-failure': captures a screenshot automatically when a test fails

Using codegen to write tests faster

Playwright includes a code generator that records your browser actions and outputs test code. Run it against your application:

npx playwright codegen http://localhost:3000

A browser window and a code panel open side by side. As you click, fill forms, and navigate, Playwright writes the corresponding test code in real time. This is a fast way to scaffold tests for complex flows. The generated code often needs cleanup (codegen tends to use CSS selectors) but it saves significant time on the initial structure.

Organising tests at scale

As your suite grows, a few conventions keep it manageable:

  • Group by feature, not by page. tests/auth/login.spec.ts and tests/auth/register.spec.ts is cleaner than one giant tests/auth.spec.ts.
  • Use test.describe for grouping related tests within a file, and test.beforeEach for shared setup.
  • Page Object Model for complex UIs. If multiple tests interact with the same component (login form, navigation), extract it into a class that encapsulates the selectors and actions. This means one place to update when the UI changes.

Running in CI

If you selected GitHub Actions during setup, you already have a workflow file at .github/workflows/playwright.yml. It runs your tests on every push and pull request. The key things it does:

  • Installs Node.js and dependencies
  • Installs Playwright browsers (npx playwright install --with-deps)
  • Runs npx playwright test
  • Uploads the HTML report as an artifact so you can download and view it after failures

For a deeper walkthrough of CI setup including caching and parallelisation, see Running Playwright Tests in GitHub Actions.

Next steps

Once you have basic tests running, the most impactful next steps are:

  • Add API testing using request fixtures alongside browser tests
  • Set up authentication state reuse so tests do not repeat the login flow on every run
  • Add visual regression testing with expect(page).toHaveScreenshot()
  • Integrate accessibility checks using @axe-core/playwright to catch WCAG issues in your test suite

If you are building out a Playwright suite and want a second pair of eyes on the architecture, or need someone to implement it entirely, get in touch. We build and maintain Playwright test suites as part of our automation service.

Related reading: Running Playwright Tests in GitHub Actions · Selenium vs Cypress vs Playwright: Which Should You Choose?

EK

Elmonds Kreslins

Lead QA Engineer

Elmonds has led QA programmes at BBC, Bupa, and multiple UK fintech startups. He founded RedQA to give growing product teams access to the same quality rigour as enterprise engineering teams, without the overhead.

QA insights, monthly

No spam. Unsubscribe any time.

Get practical QA guides, testing tips, and industry news sent straight to your inbox. Join engineers and product teams from across the UK.

Ready to Ship with Confidence?

Let's discuss how RedQA can help you deliver better software, faster. Get a free consultation and quote tailored to your project.