Back to Testing guides

Playwright End-to-End Testing: A Step-by-Step Guide

Ayooluwa Isaiah
Updated on March 23, 2024

In this tutorial, you'll explore the capabilities of Playwright by conducting end-to-end tests on the React version of the renowned TodoMVC project.

This project provides a reference for comparing the implementation of various JavaScript frameworks using the same application. Don't worry if you're unfamiliar with React; the focus here is testing with Playwright, not on React specifics.

By reading through this article, you'll learn to develop and execute Playwright test scripts, utilize its time travel debugging capabilities, and proactively identify visual regressions in development to prevent them from impacting your users.


Before proceeding with this tutorial, ensure that you have a recent version of Node.js installed, preferably the latest LTS.

If you're completely new to Playwright, I recommend reading our introductory article on Playwright testing essentials before jumping into this one.

All set? Let's get started!

Step 1 — Setting up a React demo project

Start by cloning the TodoMVC React application repository to your local machine:

git clone

Once cloned, navigate into the project directory and install the required dependencies:

cd react-todo-mvc
npm install

This step ensures all necessary libraries and tools are available for the development server. After installation, you can start the server using:

npm run dev

The server will initialize and listen on port 8080. The output will resemble:

> todomvc-react@1.0.0 dev
> webpack serve --open --config

<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8080/
<i> [webpack-dev-server] On Your Network (IPv4):
. . .

To ensure the application is running correctly, open http://localhost:8080 in your web browser. You should see the TodoMVC application interface. Try adding a few tasks to confirm that the application functions as expected.


With Playwright, you will create tests that verify key functionalities of the application, such as the ability to add and manage to-do items. These tests will demonstrate the power and simplicity of Playwright for automated browser testing.

Before moving to the next step, stop the development server. You can do this by pressing Ctrl-C in the terminal where the server is running. The server will shut down gracefully:

^C [webpack-dev-server] Gracefully shutting down. To force exit, press ^C again. Please wait...

Now that the demo project is set up, you're ready to integrate and configure Playwright for writing and running end-to-end tests.

Step 2 — Setting up Playwright

In this section, you'll install Playwright into the project and set up the browser engines required for end-to-end testing.

Begin by adding Playwright to your project. At the root directory of your project, run the following command:

npm init playwright@latest

During the installation, you'll be prompted to make several selections. Choose as follows for this tutorial:

Initializing project in 'react-todo-mvc'
✔ Do you want to use TypeScript or JavaScript? · JavaScript
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true
✔ Install Playwright operating system dependencies (requires sudo / root - can be done manually via 'sudo npx playwright install-deps')? (y/N) · true

After the installation, you will see a completion message indicating that Playwright and its dependencies are now part of your project:

. . .
We suggest that you begin by typing:

    npx playwright test

And check out the following files:
  - ./tests/example.spec.js - Example end-to-end test
  - ./tests-examples/demo-todo-app.spec.js - Demo Todo App end-to-end tests
  - ./playwright.config.js - Playwright Test configuration

Visit for more information. ✨

Happy hacking! 🎭

Next, let's configure Playwright to suit our testing requirements. Open the playwright.config.js file, which was created during the installation, in your preferred text editor:

code playwright.config.js
// @ts-check
const { defineConfig, devices } = require('@playwright/test');

 * Read environment variables from file.
// require('dotenv').config();

 * @see
module.exports = defineConfig({
testDir: './tests',
/* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See */ reporter: 'html', /* Shared settings for all the projects below. See */ use: { /* Base URL to use in actions like `await page.goto('/')`. */
baseURL: '',
/* Collect trace when retrying the failed test. See */ trace: 'on-first-retry', }, /* Configure projects for major browsers */
projects: [
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
name: 'webkit',
use: { ...devices['Desktop Safari'] },
. . .
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run playwright-server',
url: '',
reuseExistingServer: !process.env.CI,

Playwright has many options to configure how your tests are ran, but I've highlighted the most important ones for this tutorial above:

  • testDir: This option points to the ./tests directory where your test files will reside.

  • projects: It defines the browser environments for testing (Chromium, Firefox, WebKit).

  • webServer: Playwright allows you to launch a local dev server before running your tests, which is useful in development environments. The command property is the shell command to be executed, and the url is the root URL of the server once it is launched. See the docs for more configuration options.

  • use.baseURL: This specifies the root URL of your application server. It allows you to use relative paths in your test scripts instead of writing absolute URLs everywhere.

Before proceeding, you need to create the playwright script in your package.json file as follows:

"scripts": {
    "playwright-server": "webpack serve --port=8081 --config",

This script will start the development server on a different port specifically for Playwright testing.

With Playwright now set up and configured, you are all set to begin writing your first test script.

Step 3 — Writing your first test

Playwright created a tests directory containing an example test file in the previous step. Start by removing this example:

rm tests/example.spec.js

Next, create a new test file specifically for your To-do application:

code tests/todo.spec.js

In this new test file, you'll write a test to check the application's initial state. The aim is to ensure that upon a fresh load, there are no to-do items listed and the input field is in focus.

Go ahead and write the code to enforce these expectations:

const { test, expect } = require('@playwright/test');

test.describe('Initial state', () => {
  test('should be empty with focused input', async ({ page }) => {
    // Load the TodoMVC application
    await page.goto('/');

    const todoList = page.getByTestId('todo-list');

    // Make sure the list only has no items
    await expect(todoList).toBeEmpty();

    const todoInput = page.getByTestId('text-input');

    // Make sure the input is focused
    await expect(todoInput).toBeFocused();

Playwright's test and expect functions define the test and make assertions respectively. The describe() method groups related tests together and clearly describes what the test suite entails.

Inside test.describe(), the test() function defines a specific test case to validate the empty state of the to-do list and the focus on the input field.

It navigates to the TodoMVC app at http://localhost:8081, and then uses the getByTestId() locator to find elements based on their data-testid attributes.


The expect() function is subsequently employed to assert that the to-do list is empty and the input field is focused.

To run your test, execute the following command in your terminal:

npx playwright test

This command triggers the development server and runs the tests across the different browsers as configured in playwright.config.js. All tests should pass, indicating that your application's initial state behaves as expected on all major browsers.

. . .
Running 3 tests using 3 workers
  3 passed (4.9s)

To open last HTML report run:

  npx playwright show-report

Having confirmed the application's initial behavior, the upcoming section will involve testing more intricate user interactions. Additionally, you'll explore how Playwright's UI mode can assist in understanding and debugging these interactions.

Step 4 — Exploring Playwright's UI mode

In this part of the tutorial, you'll create a test to confirm that items can be successfully added to the to-do app and use Playwright's UI mode to visualize and debug the test execution.

First, update your todo.spec.js file to include a new test:

. . .

const TODO_ITEMS = ['save the world', 'go back in time', 'learn C'];

test.describe('New Todo', () => {
  test('should allow me to add todo items', async ({ page }) => {
    await page.goto('/');

    // create a new todo locator
    const todoList = page.getByTestId('todo-list');

    await todoInput.fill(TODO_ITEMS[0]);

    // Make sure the list only has one todo item.
    await expect(page.getByTestId('todo-item-label')).toHaveText([

    // Create 2nd todo.
    await todoInput.fill(TODO_ITEMS[1]);

    // Make sure the list now has two todo items.
    await expect(page.getByTestId('todo-item-label')).toHaveText([

This test ensures that users can add items to the to-do list and checks that the list updates accordingly.

It starts by loading the application and selecting the input field for new to-dos using its data-testid, and then proceeds in stages:

  1. The first item from the TODO_ITEMS array is entered into the input field and the Enter key press is simulated.
  2. The to-do list is then checked to verify that it contains only this first item.
  3. The second item from TODO_ITEMS is entered similarly.
  4. The to-do list is checked again to verify that it now includes both the first and second items

To observe how the test runs, use the following command

npx playwright test -g 'New Todo' --ui

When you run this command, Playwright's UI mode window will open. It is an interactive tool that allows you to visually step through tests, inspect the DOM at various stages, view timelines of test executions, and analyze actions, logs, and errors in real-time. This feature significantly enhances the end-to-end testing experience by making it easier to understand, debug, and refine your test scripts.


When UI mode is activated, the test is initially paused. To resume execution, click the Play icon next to the test title. It should proceed to execute the test and complete without errors:


At the top of the trace, you'll see a timeline of the test execution. You can hover on any portion of the timeline to see a snapshot of the test, or select between two points to filter only the actions and logs for that portion of the test.


The Actions tab enumerates each action taken in the test, including the locators used and their execution duration. Clicking on an action in the list displays the changes it caused in the application. You can also check out the Before and After tabs to visually inspect the application state before and after the action.


You can inspect the DOM at any test stage by clicking the external link icon at the top right of a DOM snapshot. This opens a window where you can use the browser DevTools for in-depth examination when troubleshooting your scripts.


Below the DOM snapshot, several tabs provide additional information:

  • Locator: Used to select locators from the DOM snapshot that can subsequently be copied into the test script.
  • Source: Shows the code for the highlighted action.
  • Call: Displays timing and locator details.
  • Log: Contains internal Playwright logs.
  • Errors: Lists any errors encountered during the test.
  • Console and Network: Show application logs and network requests.
  • Attachments: Useful for visual comparisons in regression testing.

By utilizing these features, you can effectively debug and understand the behavior of your Playwright scripts to ensure that they accurately validate the application's functionality being tested.

Step 5 — Detecting visual regressions with Playwright

Visual regression testing focuses on identifying unintentional changes in the visual appearance of an application. It compares the current visual look of elements or screens in an application against a set of baseline images captured earlier to ensure that UI elements appear as intended across different environments and browsers, or after code changes.

Playwright makes it easy to check for visual regressions in your applications by following this general process:

  1. First, it establishes a set of baseline images that represent the expected state of the UI elements or pages. These serve as the reference point for future comparisons.

  2. Next, it captures the current state of the UI during the test execution.

  3. It then compares these images against the reference. Any discrepancies are flagged as potential regressions, which might be due to changes in CSS, layout, JavaScript updates, or browser behavior.

Under the hood, Playwright uses the pixelmatch library, a pixel-level image comparison library. When running a visual regression test for the first time, it generates and saves a reference screenshot as the baseline.

Let's see this in action by adding the following test to your todo.spec.js file:

. . .
test.describe('Visual comparison', () => {
  test('initial state', async ({ page }) => {
    await expect(page).toHaveScreenshot();

This test uses the toHaveScreenshot() method to capture and compare the current state of the page with the expected baseline image.

Before you execute the test, refactor the test script as shown below:

const { test, expect } = require('@playwright/test');

test.beforeEach(async ({ page }) => {
await page.goto('/');
test.describe('Initial state', () => { test('should be empty with focused input', async ({ page }) => { const todoList = page.getByTestId('todo-list'); . . . }); }); const TODO_ITEMS = ['save the world', 'go back in time', 'learn C']; test.describe('New Todo', () => { test('should allow me to add todo items', async ({ page }) => { const todoList = page.getByTestId('todo-list'); . . . }); }); test.describe('Visual comparison', () => { test('initial state', async ({ page }) => { await expect(page).toHaveScreenshot(); }); });

To avoid repeating the line that navigates to root path in each test, the The beforeEach() hook ensures that the page is loaded before each test is executed. This helps maintain a clean test structure and ensures that certain preconditions are consistently met before executing every test.

Now, go ahead and execute the Visual comparison suite using the command below:

npx playwright test -g 'Visual'

Initially, the test will fail due to the absence of a baseline image, commonly known as a "golden file". This failure is expected and part of establishing your initial baseline.

npx playwright test -g 'Visual'
. . .
  3 failed
    [chromium] › todo.spec.js:53:3 › Visual comparison › initial state ───────────────────────
    [firefox] › todo.spec.js:53:3 › Visual comparison › initial state ────────────────────────
    [webkit] › todo.spec.js:53:3 › Visual comparison › initial state ────────────────────────


After running the command, an HTML report will open in your default browser, showing that the test failed across all browsers. This failure occurs because there are no existing baseline images to compare against. The report will include a Screenshots section to view the captured images.


These initial screenshots are the reference images that are saved within your tests directory as follows:

tree tests
├── example.spec.js
├── todo.spec.js
└── todo.spec.js-snapshots
    ├── Visual-comparison-initial-state-1-chromium-linux.png
    ├── Visual-comparison-initial-state-1-firefox-linux.png
    └── Visual-comparison-initial-state-1-webkit-linux.png

The name of each reference file is a concatenation of the suite name, test name, browser engine and platform. You can also provide a custom screenshot name by providing an argument to toHaveScreenshot():

await expect(page).toHaveScreenshot('todomvc-initial-state.png');

This will yield file names such as:


Once a baseline screenshot is established in your Playwright setup, all subsequent test executions will compare the captured UI state to this baseline reference.

Quit the HTML report server with Ctrl-C, then re-run the visual regression test as follows:

npx playwright test -g 'Visual'

You should see a successful test run, indicating no visual discrepancies:

. . .
Running 3 tests using 3 workers
  3 passed (5.3s)
. . .

To demonstrate Playwright's visual regression reporting capabilities, introduce a minor change in your application. For example, update the color of a header element in the header.jsx file:

import { useCallback } from "react";
import { Input } from "./input";

import { ADD_ITEM } from "../constants";

export function Header({ dispatch }) {
    const addItem = useCallback((title) => dispatch({ type: ADD_ITEM, payload: { title } }), [dispatch]);

    return (
        <header className="header" data-testid="header">
<h1 style={{color: "blue"}}>todos</h1>
<Input onSubmit={addItem} label="New Todo Input" placeholder="What needs to be done?" /> </header> ); }

This modification changes the header's text color to blue, mimicking a typical change during development.

Execute the visual regression test again:

npx playwright test -g 'Visual'

This time, expect the test to fail as the UI's current state deviates from the baseline:

. . .
  3 failed
    [chromium] › todo.spec.js:53:3 › Visual comparison › initial state ───────────────────────
    [firefox] › todo.spec.js:53:3 › Visual comparison › initial state ────────────────────────
    [webkit] › todo.spec.js:53:3 › Visual comparison › initial state ────────────────────────

Upon failure, the test report automatically opens in your browser, providing detailed insights:


Open any of the failure items and scroll to the Image mismatch section to compare the new screenshot with the reference. The following tabs are presented:

  • Diff highlights the affected elements in red,
  • Actual displays the current version of the screenshot,
  • Expected displays the reference screenshot,
  • Side by side places the expected and actual results side by side,
  • Slider allows for interactive comparison with a draggable slider.

When faced with a test failure, there are two primary courses of action:

  1. Unintentional changes: If the visual difference is not intended, investigate the cause of the issue and revert your changes or correct your code until the test passes.

  2. Intentional updates: If the change is deliberate and should become the new standard, update the reference screenshot with the following command:

npx playwright test -g 'Visual' --update-snapshots

It regenerates the baseline images, aligning them with the new visual state of your application:

. . .

Running 3 tests using 3 workers
[chromium] › todo.spec.js:53:3 › Visual comparison › initial state
/home/ayo/dev/betterstack/demo/react-todo-mvc/tests/todo.spec.js-snapshots/Visual-comparison-initial-state-1-chromium-linux.png is re-generated, writing actual.
[firefox] › todo.spec.js:53:3 › Visual comparison › initial state
/home/ayo/dev/betterstack/demo/react-todo-mvc/tests/todo.spec.js-snapshots/Visual-comparison-initial-state-1-firefox-linux.png is re-generated, writing actual.
[webkit] › todo.spec.js:53:3 › Visual comparison › initial state
/home/ayo/dev/betterstack/demo/react-todo-mvc/tests/todo.spec.js-snapshots/Visual-comparison-initial-state-1-webkit-linux.png is re-generated, writing actual.
  3 passed (5.1s)

. . .

Once the baseline images are updated, the tests should pass, reflecting the new accepted state of the UI. The updated screenshots should then be committed to source control to serve as the new reference for future test runs.

Final thoughts

In this guide, you've learned to create, execute, and debug end-to-end tests for a React application with Playwright. We also discussed the importance of regression testing for maintaining the visual integrity of application frontends and demonstrated how to implement such tests with Playwright effectively.

Our upcoming article will shift focus to deploying Playwright tests in CI environments, and offer tips for diagnosing and resolving test failures in a production environment.

Thanks for reading, and stay tuned!

Author's avatar
Article by
Ayooluwa Isaiah
Ayo is the Head of Content at Better Stack. His passion is simplifying and communicating complex technical ideas effectively. His work was featured on several esteemed publications including, Digital Ocean, and CSS-Tricks. When he’s not writing or coding, he loves to travel, bike, and play tennis.
Got an article suggestion? Let us know
Next article
9 Playwright Best Practices and Pitfalls to Avoid
Explore 9 essential Playwright best practices to enhance the reliability, efficiency, and effectiveness of your end-to-end tests
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github