Skip to main content

E2E Testing with Playwright

Playwright is a reliable End-to-End (E2E) testing framework developed by Microsoft. It supports all modern browsers including Chromium, WebKit, and Firefox.

Key Features

  • Cross-browser Support: Works on Chrome, Edge, Firefox, Safari (WebKit).
  • High Reliability: Auto-wait feature waits for elements to appear before operating, reducing flaky tests.
  • Powerful Tools: Comes standard with Code generation (Codegen), Playwright Inspector, Trace Viewer, etc.
  • Complete Isolation: Each test runs in an independent browser context, so there is no state interference.

Installation and Setup

To introduce Playwright to your project, run the following command.

npm init playwright@latest

A setup wizard will appear, so select the following items.

  • TypeScript or JavaScript (TypeScript recommended)
  • Name of test folder (Default: tests)
  • Whether to add GitHub Actions workflow

Basic Structure of Tests

Playwright test files are usually placed in the tests directory.

tests/example.spec.ts
import { test, expect } from '@playwright/test';

test('Homepage title is correct', async ({ page }) => {
// Access page
await page.goto('https://playwright.dev/');

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

test('Get Started link works', async ({ page }) => {
await page.goto('https://playwright.dev/');

// Click "Get started" link
await page.getByRole('link', { name: 'Get started' }).click();

// Verify URL contains "/docs/intro"
await expect(page).toHaveURL(/.*intro/);
});

Locators

Playwright has recommended locators to retrieve elements in a way close to user operations.

  • page.getByRole(): Retrieve by role such as button, link, heading (Most recommended)
  • page.getByText(): Retrieve by text content
  • page.getByLabel(): Retrieve by form label
  • page.getByPlaceholder(): Retrieve by placeholder text
  • page.getByAltText(): Retrieve by image alt text
  • page.getByTestId(): Retrieve by data-testid attribute
// Click button
await page.getByRole('button', { name: 'Submit' }).click();

// Text input
await page.getByLabel('Username').fill('user123');

Auto-waiting

One of the major features of Playwright is that it automatically waits for the element to become actionable before performing an operation. This significantly reduces the need to write sleep or explicit wait processing.

For example, when executing click(), Playwright waits until all of the following conditions are met:

  1. Attached to DOM
  2. Visible: Not display: none or visibility: hidden
  3. Stable: Animation has stopped
  4. Receives Events: Not hidden by other elements
  5. Enabled: No disabled attribute

These checks are repeated until timeout (default 30 seconds).

Assertions

Use the expect function to verify the state of elements or the page. These automatically retry.

// Is element visible
await expect(locator).toBeVisible();

// Does it contain text
await expect(locator).toContainText('Complete');

// Is input value correct
await expect(locator).toHaveValue('user123');

// Is checkbox checked
await expect(locator).toBeChecked();

Running Tests

Running from Command Line

Run all tests:

npx playwright test

Run only specific file:

npx playwright test tests/example.spec.ts

Run with browser displayed (not headless mode):

npx playwright test --headed

UI Mode

You can run and debug tests in interactive UI mode. Time travel debugging etc. are possible.

npx playwright test --ui

Viewing Report

To view HTML report after test execution:

npx playwright show-report

VS Code Extension

If you are using VS Code, it is strongly recommended to install the official Playwright Test for VSCode extension. This allows you to run and debug tests directly from the editor.

Frontend Implementation to Prevent Flaky Tests

Test stability (elimination of Flakiness) depends heavily not only on test code but also on frontend implementation.

1. Utilizing Test Attributes (data-testid)

CSS classes and IDs may change for styling. By assigning the test-specific attribute data-testid, you can create robust locators that are not affected by design changes.

// Not recommended implementation
<button className="btn-primary-large">Submit</button>

// Recommended implementation
<button data-testid="submit-button">Submit</button>

On the test side, retrieve with page.getByTestId('submit-button').

2. Semantic HTML and ARIA Roles

Playwright recommends using getByRole. By using appropriate HTML tags (<button>, <nav>, <h1>, etc.) and ARIA attributes, you can improve accessibility and make testing easier at the same time.

3. Visualization of Loading State

Display a spinner or skeleton screen during asynchronous processing, and implement it so that they disappear after processing is complete. This allows you to write clear wait conditions like "wait until loading disappears" on the test code side.

// Guarantee data loading completion by waiting for loading to disappear
await expect(page.getByTestId('loading-spinner')).not.toBeVisible();

4. Control of Dynamic Content

Ensure that dates, random numbers, animations, etc. do not affect results during test execution. Consider using mock data or settings to disable animations as needed.

Best Practices

  1. Locator Priority: Use attributes visible to users like getByRole as much as possible, and avoid CSS selectors and XPath.
  2. Web First Assertions: Use await expect(...) to appropriately wait for asynchronous state changes.
  3. Independence: Make each test independent and do not depend on the execution order or state of other tests.
  4. Utilize Codegen: Use npx playwright codegen to record operations and generate test code templates.