Getting Started with Playwright: A Beginner's Guide to Browser Automation
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
testsdirectory 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. Thepageobject 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 URLsretries: 2in CI: retries flaky tests twice before marking them failed, which reduces noise from infrastructure issuestrace: 'on-first-retry': records a full trace (DOM snapshots, network, console) when a test is retried, making debugging much easierscreenshot: '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.tsandtests/auth/register.spec.tsis cleaner than one gianttests/auth.spec.ts. - Use
test.describefor grouping related tests within a file, andtest.beforeEachfor 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
requestfixtures 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/playwrightto 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?
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.
Related articles
Selenium vs Cypress vs Playwright: Full 2026 Comparison
Playwright, Cypress, or Selenium - which automation framework is right for your team in 2026? We compare speed, browser support, language options, debugging, and real-world fit.
Playwright vs Cypress vs Selenium: 2026 Full Comparison
Which test automation framework should your team use in 2026? We compare Playwright, Cypress, and Selenium across speed, developer experience, browser support, and CI/CD integration.
How to Set Up Playwright with GitHub Actions: Step-by-Step
A complete guide to integrating Playwright tests into your GitHub Actions CI/CD pipeline, from initial setup to parallel test execution and HTML report publishing.
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.