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

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](https://todomvc.com/).

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.

## Prerequisites

Before proceeding with this tutorial, ensure that you have a recent version of
Node.js installed, preferably [the latest LTS](https://nodejs.org/en/download).

If you're completely new to Playwright, I recommend reading our [introductory
article on Playwright testing essentials](https://betterstack.com/community/guides/testing/playwright-intro/) 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:

```command
git clone https://github.com/betterstack-community/react-todo-mvc
```

Once cloned, navigate into the project directory and install the required
dependencies:

```command
cd react-todo-mvc
```

```command
npm install
```

This step ensures all necessary libraries and tools are available for the
development server. After installation, you can start the server using:

```command
npm run dev
```

The server will initialize and listen on port 8080. The output will resemble:

```text
[output]
> todomvc-react@1.0.0 dev
> webpack serve --open --config webpack.dev.js

<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8080/
<i> [webpack-dev-server] On Your Network (IPv4): http://192.168.0.189:8080/
. . .
```

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.

![react-app.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/901ab093-e050-473e-1358-cd89094d7300/lg2x =2258x1428)

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:

```text
[output]
^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:

```command
npm init playwright@latest
```

During the installation, you'll be prompted to make several selections. Choose
as follows for this tutorial:

```text
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:

```text
[output]
. . .
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 https://playwright.dev/docs/intro 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:

```command
code playwright.config.js
```

```javascript
[label playright.config.js]
// @ts-check
const { defineConfig, devices } = require('@playwright/test');

/**
 * Read environment variables from file.
 * https://github.com/motdotla/dotenv
 */
// require('dotenv').config();

/**
 * @see https://playwright.dev/docs/test-configuration
 */
module.exports = defineConfig({
[highlight]
  testDir: './tests',
[/highlight]
  /* 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 https://playwright.dev/docs/test-reporters */
  reporter: 'html',
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    /* Base URL to use in actions like `await page.goto('/')`. */
    [highlight]
    baseURL: 'http://127.0.0.1:8081',
    [/highlight]

    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
    trace: 'on-first-retry',
  },

  /* Configure projects for major browsers */
[highlight]
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },

    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },

    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },

    . . .
  ],
[/highlight]

  /* Run your local dev server before starting the tests */
  [highlight]
  webServer: {
    command: 'npm run playwright-server',
    url: 'http://127.0.0.1:8081',
    reuseExistingServer: !process.env.CI,
  },
  [/highlight]
});
```

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](https://playwright.dev/docs/test-webserver) 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:

```json
[label package.json]
"scripts": {
    "playwright-server": "webpack serve --port=8081 --config webpack.dev.js",
},
```

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:

```command
rm tests/example.spec.js
```

Next, create a new test file specifically for your To-do application:

```command
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:

```javascript
[label tests/todo.spec.js]
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.

![playwright-locator.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/202558f4-7831-41af-7696-fb018fb42900/md1x =1996x1516)

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:

```command
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.

```text
[output]
. . .
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:

```javascript
[label tests/todo.spec.js]
. . .

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]);
    await todoInput.press('Enter');

    // Make sure the list only has one todo item.
    await expect(page.getByTestId('todo-item-label')).toHaveText([
      TODO_ITEMS[0],
    ]);

    // Create 2nd todo.
    await todoInput.fill(TODO_ITEMS[1]);
    await todoInput.press('Enter');

    // Make sure the list now has two todo items.
    await expect(page.getByTestId('todo-item-label')).toHaveText([
      TODO_ITEMS[0],
      TODO_ITEMS[1],
    ]);
  });
});
```

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

```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.

![playwright-ui-mode.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/1435d157-ee30-4376-8de2-7fa86034e500/lg2x =2804x1918)

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:

![playwright-ui-run.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/8d1f5fed-4173-4600-dbe0-5386fc544900/md1x =2804x1918)

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.

![playwright-timeline.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/e1b3c064-78b2-49e9-db11-0e2a9a09f100/orig =2628x315)

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.

![playwright-action.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/daea7e3f-9cbc-4af3-7a65-745a2d2aed00/orig =2051x925)

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.

![playwright-inspect.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/c855e595-2153-4e6d-a2d0-c0db729d3700/orig =2154x1600)

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](https://github.com/mapbox/pixelmatch), 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:

```javascript
[label tests/todo.spec.js]
. . .
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:

```javascript
[label tests/todo.spec.js]
const { test, expect } = require('@playwright/test');

[highlight]
test.beforeEach(async ({ page }) => {
  await page.goto('/');
});
[/highlight]

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:

```command
npx playwright test -g 'Visual'
```

Initially, the test will fail due to the absence of a baseline image, commonly
known as a
"[golden file](https://softwareengineering.stackexchange.com/questions/358786/what-are-golden-files)".
This failure is expected and part of establishing your initial baseline.

```command
npx playwright test -g 'Visual'
```

```text
[output]
. . .
  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 ────────────────────────
```
![playwright-visuals-1.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/3640a24d-00c4-4efb-0860-d34606c8bc00/md2x =2542x1036)

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.

![playwright-visuals-2.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/c68f3ec6-131c-43f4-1c48-589c8fa57800/public =2542x2324)

These initial screenshots are the reference images that are saved within your
`tests` directory as follows:

```command
tree tests
```

```text
[output]
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()`:

```javascript
await expect(page).toHaveScreenshot('todomvc-initial-state.png');
```

This will yield file names such as:

```text
todomvc-initial-state-chromium-linux.png
todomvc-initial-state-firefox-linux.png
todomvc-initial-state-webkit-linux.png
```

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:

```command
npx playwright test -g 'Visual'
```

You should see a successful test run, indicating no visual discrepancies:

```text
[output]
. . .
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:

```javascript
[label src/todo/components/header.jsx]
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">
            [highlight]
            <h1 style={{color: "blue"}}>todos</h1>
            [/highlight]
            <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:

```command
npx playwright test -g 'Visual'
```

This time, expect the test to fail as the UI's current state deviates from the
baseline:

```text
[output]
. . .
  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:

![playwright-visual-comparison.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/684bb183-a43a-43b0-0a10-d18761ac5000/lg2x =2364x2090)

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:

```command
npx playwright test -g 'Visual' --update-snapshots
```

It regenerates the baseline images, aligning them with the new visual state of
your application:

```text
[output]
. . .

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!
