Test TodoMVC with Playwright

Build a cross-browser TypeScript test suite for the Playwright TodoMVC demo.

Introduction

30 Second Summary

A browser feature can work perfectly today and break after the next code change. Repeating every click by hand makes reliable releases slow.

In this project, you will build a maintainable TodoMVC test suite with Playwright Test and TypeScript. You will automate user journeys across multiple browser engines and inspect the results in a test report.

What You'll Build

When you run your finished suite, you will watch real browser engines complete TodoMVC journeys from a single command.

By the end of this project, you'll have:

  • Two automated TodoMVC journeys that add todos. They also complete a todo. The tests confirm the visible results after each journey.
  • A reusable TodoPage class based on the Page Object Model. It keeps selectors and browser actions in one place.
  • Cross-browser evidence from Chromium, Firefox, and WebKit. An HTML report shows the result for every browser project.
  • Secret Mission: Generate separate tests from a small array of todo names without copying the test body.

Are there any prerequisites?

You need a Mac running macOS 14 or newer with internet access. Basic comfort opening files or a terminal is enough.

Before We Start

This checkpoint connects the browser test suite you are about to build with the skills you want to demonstrate. A clear purpose gives each later test a goal.

Prepare the Playwright Project

Your TodoMVC test suite needs a dependable local foundation before it can control a browser. A mismatched runtime or missing browser binary would stop every later test.

You will use Visual Studio Code to give the project one visible home. You will verify Node.js before installing Playwright Test at the pinned version.

In this step, get ready to:
  • Create the todo-playwright-tests folder with its baseline package.json file.
  • Verify a supported Node.js version before installing the project dependency and browser binaries.
  • Add a starter TypeScript configuration for Chromium with HTML reporting.
Create the project files

A small manual project makes every framework layer visible. You can see exactly which file controls dependencies before generated files appear.

Why a manual project?

A manual setup keeps the framework structure focused on files you understand. Each new file has a clear job in the test suite.

  • Press Cmd+Space to open Spotlight.
  • Type Visual Studio Code into Spotlight.
  • Press Enter to open Visual Studio Code.

You will see Visual Studio Code ready for a folder.

  • Select File from the macOS menu bar.
  • Select Open Folder....
  • Select Desktop in the file dialog.
  • Select New Folder.
  • Enter todo-playwright-tests as the folder name.
  • Select Open.

The todo-playwright-tests folder now appears in the Explorer sidebar.

  • Select the Explorer view in the Activity Bar.
  • Select New File in the Explorer view.
  • Enter package.json as the file name.
  • Press Enter to create the file.
  • Define the project package by replacing the file contents with this code:
{
  "name": "todo-playwright-tests",
  "version": "1.0.0",
  "private": true,
  "devDependencies": {
    "@playwright/test": "1.62.1"
  }
}

What does this file define?

  • The package name identifies this local project.
  • The private setting protects the package from accidental publication.
  • The devDependencies section pins @playwright/test to 1.62.1.
  • Save package.json by pressing Cmd+S.

You will see package.json listed inside todo-playwright-tests in the Explorer sidebar.

Package file missing?

Check that todo-playwright-tests is the folder shown at the top of the Explorer sidebar. Create package.json inside that folder if it appears elsewhere.

Ask for help with the file location:

✔️ Awesome, I've got everything!

Your saved package file is ready to drive the dependency installation.

ⓧ I'd like to double check the full code

{
  "name": "todo-playwright-tests",
  "version": "1.0.0",
  "private": true,
  "devDependencies": {
    "@playwright/test": "1.62.1"
  }
}
Verify Node.js and install Playwright

Playwright Test supports the latest Node.js 22.x, 24.x, or 26.x release lines. Checking the runtime now protects the dependency installation from compatibility problems.

  • Select Terminal from the Visual Studio Code menu bar.
  • Select New Terminal.
  • Check the installed Node.js version by running this command:
node --version

What does this command check?

The command prints the Node.js version used by this terminal. Its major number determines whether the current runtime is supported.

✔️ I see a supported version

Your terminal shows Node.js 22.x, 24.x, or 26.x. Your runtime is ready for the pinned Playwright Test package.

ⓧ I see another major version

Your installed major version sits outside the supported release lines for this project. Update to the current Node.js LTS release before installing dependencies.

  • Open the official Node.js download page.
  • Download Node.js v24.20.0 LTS for macOS.
  • Run the downloaded installer.
  • Return to the Visual Studio Code terminal from earlier.
  • Check the updated Node.js version by running this command:
node --version

What does this command confirm?

The new version output confirms which Node.js runtime the reopened terminal can access.

Continue when the major version is 22, 24, or 26.

ⓧ Command not found

The terminal cannot find a Node.js installation. Installing the current LTS release also provides npm.

  • Open the official Node.js download page.
  • Download Node.js v24.20.0 LTS for macOS.
  • Run the downloaded installer.
  • Return to the Visual Studio Code terminal from earlier.
  • Check the installed Node.js version by running this command:
node --version

What does this command confirm?

A version number confirms that the terminal can now access the installed Node.js runtime.

Continue when the major version is 22, 24, or 26.

Still seeing the previous Node.js version?

Quit Visual Studio Code after the installer finishes. Reopen the todo-playwright-tests folder so a fresh terminal loads the updated runtime.

Get help with the runtime path:

The supported runtime can now read package.json and install its pinned development dependency. npm also records the resolved installation in a lock file.

  • Install the dependency from package.json by running this command:
npm install

What does this command create?

  • The command installs @playwright/test at the version pinned in package.json.
  • The node_modules folder stores the installed packages.
  • The generated package-lock.json records the resolved dependency tree.

The Explorer sidebar now lists node_modules and package-lock.json inside the project folder.

Dependency installation failed?

Confirm that the terminal prompt is inside todo-playwright-tests. Check that package.json contains valid JSON with matching braces.

Ask for help with the installation error:

The package provides the test runner. Playwright-managed browser binaries provide the engines that the runner controls.

This download may take several minutes on the first run. The terminal can stay busy while it retrieves the browser files.

  • Install the Playwright-managed browsers by running this command:
npx playwright install

What does this command install?

The Playwright command downloads its managed browser binaries. These binaries let later tests run against the supported browser engines.

You will see browser download activity finish before the terminal prompt returns.

Browser download interrupted?

Check your internet connection before rerunning the same install command. A restricted network can block the browser downloads.

Ask for help with the browser installation:

Add the starter Playwright configuration

A Playwright configuration defines where tests live. It also controls reporting and browser projects from one TypeScript file.

This starter configuration uses one browser project. The focused setup gives your first test a short feedback loop.

  • Select New File in the Explorer view.
  • Enter playwright.config.ts as the file name.
  • Press Enter to create the file.
  • Define the starter test configuration by replacing the file contents with this code:
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  reporter: [['html', { open: 'never' }]],
  use: {
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

What does this configuration do?

  • The testDir setting points the runner to the future tests folder.
  • The reporter setting creates an HTML report without opening it automatically.
  • The trace setting records diagnostic detail on the first retry.
  • The project uses the Desktop Chrome device preset for the Chromium run.
  • Save playwright.config.ts by pressing Cmd+S.

You will see playwright.config.ts beside package.json in the Explorer sidebar.

Configuration showing syntax errors?

Compare every quote and bracket with the reference below. Confirm that the file name ends with .ts.

Ask for help comparing the configuration:

✔️ Awesome, I've got everything!

Your starter configuration is saved with the Chromium project and HTML reporter.

ⓧ I'd like to double check the full code

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

export default defineConfig({
  testDir: './tests',
  reporter: [['html', { open: 'never' }]],
  use: {
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

Before you run the final check, which Node.js major version and Playwright Test version do you expect the terminal to report?

  • Verify both installed versions by running these commands:
node --version
npx playwright --version

What do these checks prove?

  • The first command confirms the Node.js runtime used by the project terminal.
  • The second command confirms the Playwright version resolved from the installed dependency.

You will see a Node.js major version of 22, 24, or 26. The next line shows Version 1.62.1.

That is the foundation locked in. Your project now has a supported runtime, a pinned test framework, managed browsers, and a starter configuration.

Your Playwright project is ready to run browser automation. Next up, you will write one complete TodoMVC journey and watch it pass in a visible browser.

Write Your First Automated Test

Your Playwright Test setup is ready to control real browser engines. The next goal is a complete TodoMVC journey you can watch from start to finish.

A passing test gives you a reliable coding loop before framework abstractions add complexity. You will write one TypeScript test with the built-in page fixture.

In this step, get ready to:
  • Create tests/todo.spec.ts with one automated TodoMVC journey.
  • Connect each part of the journey to arrange-act-assert.
  • Run the test in a visible Chromium window.
Turn the journey into a test

A test case converts a user journey into repeatable browser actions. This journey adds one todo before checking its count and visible text.

  • Use the Explorer sidebar in Visual Studio Code to create a tests folder inside todo-playwright-tests.

You should see tests listed beneath the project name in the Explorer sidebar.

  • Use the Explorer sidebar to create todo.spec.ts inside tests.

You should see todo.spec.ts open in an editor tab.

  • Add the first browser journey to tests/todo.spec.ts by copying this code:
import { test, expect } from '@playwright/test';

test('adds one todo item', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
  await page.getByPlaceholder('What needs to be done?').fill('Learn Playwright');
  await page.getByPlaceholder('What needs to be done?').press('Enter');

  await expect(page.getByTestId('todo-item')).toHaveCount(1);
  await expect(page.getByTestId('todo-item')).toContainText('Learn Playwright');
});

What does this test do?

  • The import provides the test declaration plus the assertion tools.
  • The page fixture gives this test an isolated browser page.
  • Each locator finds an element through its placeholder or test ID.
  • Each await keeps the browser operations in sequence.
  • The web-first assertions retry their checks while the page updates.
  • Save tests/todo.spec.ts.
  • Confirm the editor tab no longer shows an unsaved-change marker.

Test file in the wrong place?

Make sure todo.spec.ts sits inside tests. Your configuration scans ./tests for test files.

If Visual Studio Code flags a syntax issue, compare every quote and bracket with the full file below.

Help me check the location and syntax of my Playwright test file.

✔️ Awesome, I've got everything!

Your first test file is ready. Make sure you saved it before moving on.

ⓧ I'd like to double check the full code

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

test('adds one todo item', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
  await page.getByPlaceholder('What needs to be done?').fill('Learn Playwright');
  await page.getByPlaceholder('What needs to be done?').press('Enter');

  await expect(page.getByTestId('todo-item')).toHaveCount(1);
  await expect(page.getByTestId('todo-item')).toContainText('Learn Playwright');
});
Map the testing loop

The arrange-act-assert pattern gives a test a clear story. Arrange establishes the starting state. Act performs the user behavior. Assert checks the result.

  • Identify the navigation line as arrange because it establishes TodoMVC as the starting page.
  • Identify the input fill line as act because it enters the todo text.
  • Identify the Enter key line as act because it submits the todo.
  • Identify the count expectation as assert because it checks the number of todo items.
  • Identify the text expectation as assert because it checks the content shown to the user.
Prove the journey works

Headed mode keeps the browser visible while the test runs. This gives you direct evidence that the code performs the same journey a user would.

Before you run it, predict the browser change that would prove both assertions can pass.

  • Run the test with a visible browser window by running this command:
npx playwright test --headed

What does this command do?

The test command finds the test inside tests. The --headed flag keeps Chromium visible during the journey.

You should see Chromium load TodoMVC. The input fills with Learn Playwright before the list shows one todo item.

The browser closes after the journey finishes. Your terminal reports that the test passed.

That is your first complete browser test working from start to finish. Your code now produces visible proof of the user journey.

Browser journey not passing?

If no browser opens, confirm the terminal is still operating inside todo-playwright-tests. Also confirm that the Playwright-managed browsers remain installed from the previous step.

If an assertion fails, compare the todo text plus the expected count with tests/todo.spec.ts above.

Help me diagnose why my first headed Playwright test is not passing.

Your first automated journey now passes in a real browser. Next, you will make its expectation wrong on purpose so the failure evidence can teach you how to debug.

Investigate a Failing Test

The passing TodoMVC journey proved that the browser can add one item. A green result gives you no practice reading the evidence behind a failure.

Playwright UI Mode places each diagnostic clue in one workspace. You will change one oracle value before tracing the result.

In this step, get ready to:
  • Change the expected todo count without changing the browser actions.
  • Run the test in Playwright UI Mode.
  • Trace the result through the error details, source line, action log, and DOM snapshot.
Change the test oracle

An assertion serves as a test oracle. It defines the condition Playwright uses to judge the observed behavior.

  • In the Visual Studio Code window from earlier, return to tests/todo.spec.ts.
  • Find this count assertion:
  await expect(page.getByTestId('todo-item')).toHaveCount(1);

What Does This Line Decide?

The 1 means the locator must resolve to exactly one todo item. This matches the single item added by the browser action.

  • Replace the expected count of 1 with 2 so the line looks like this:
  await expect(page.getByTestId('todo-item')).toHaveCount(2);

What Changed?

Only the expected count is now 2. The navigation and item-creation actions remain untouched.

  • Save tests/todo.spec.ts.

✔️ Awesome, I've got everything!

Your file now contains the controlled oracle change.

ⓧ I'd like to double check the full code

  • Compare your saved tests/todo.spec.ts with this complete version:
import { test, expect } from '@playwright/test';

test('adds one todo item', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
  await page.getByPlaceholder('What needs to be done?').fill('Learn Playwright');
  await page.getByPlaceholder('What needs to be done?').press('Enter');

  await expect(page.getByTestId('todo-item')).toHaveCount(2);
  await expect(page.getByTestId('todo-item')).toContainText('Learn Playwright');
});
Run the test in UI Mode

UI Mode keeps the test runner available while you investigate. Its diagnostic views connect the failed assertion to the browser actions that came before it.

  • Launch UI Mode from the terminal from earlier by running this command:
npx playwright test --ui

What Does This Command Do?

This command opens Playwright UI Mode for the current test suite. The workspace keeps the test list and diagnostic evidence together.

  • Select adds one todo item from the test list.

Before you run the selected test, which count do you think the page will contain when the assertion checks it?

  • Run the selected test from its row in the test list.

You will see the test marked as failed. The count error reports an expectation of 2 while the page contains one todo item.

UI Mode Not Opening?

  • Return to the terminal from earlier.
  • Confirm that its prompt points to the todo-playwright-tests folder.
  • Run the UI Mode command again.

Get help diagnosing why UI Mode does not open.

Read the failure evidence

The red result is only the starting signal. A diagnosis comes from checking whether every piece of evidence tells the same story.

  • Open the failed adds one todo item test from the test list.
  • Inspect the error details to compare the expected count of 2 with the observed count of 1.
  • Inspect the source line to identify the count assertion that produced the failure.
  • Review the action log to confirm that the test filled the input with Learn Playwright.
  • Inspect the DOM snapshot to confirm that one todo item contains Learn Playwright.

What Did the Evidence Prove?

The application created one item correctly. The test oracle expected two.

The failure comes from the assertion in tests/todo.spec.ts. The browser actions and TodoMVC behavior remain correct.

You have traced the red result to a faulty oracle while confirming that TodoMVC still behaves correctly. Next up, you will use that evidence to reshape the suite into a maintainable framework.

Refactor with a Page Object

You used Playwright UI Mode to prove the TodoMVC app created one item correctly. You also traced the failure to the test's incorrect count.

That diagnosis is a real testing win. You found a broken oracle without blaming the application.

A Page Object Model centralizes repeated selectors behind a reusable TodoPage class. The tests can then describe user intent through methods such as addTodo().

In this step, get ready to:
  • Create a typed TodoPage class that centralizes TodoMVC locators.
  • Encapsulate every TodoMVC interaction in reusable methods.
  • Verify two isolated tests through the shared page object.
Create the TodoPage class

The page object needs typed references to the browser page. It also needs reusable locators for the TodoMVC controls.

Using TypeScript types makes those fields clear while you write the class. The constructor creates each locator once for every test to reuse.

  • Create a pages folder inside the open todo-playwright-tests folder using the Visual Studio Code file sidebar.
  • Create todo-page.ts inside pages using the same sidebar.
  • Build the class foundation by pasting this code into pages/todo-page.ts:
import type { Locator, Page } from '@playwright/test';

export class TodoPage {
  readonly page: Page;
  readonly newTodoInput: Locator;
  readonly todoItems: Locator;
  readonly todoTitles: Locator;

  constructor(page: Page) {
    this.page = page;
    this.newTodoInput = page.getByPlaceholder('What needs to be done?');
    this.todoItems = page.getByTestId('todo-item');
    this.todoTitles = page.getByTestId('todo-title');
  }

  async goto() {
    await this.page.goto('https://demo.playwright.dev/todomvc/');
  }

  async addTodo(text: string) {
    await this.newTodoInput.fill(text);
    await this.newTodoInput.press('Enter');
  }
}

What does this code do?

  • The Page type represents the isolated browser page supplied to a test.
  • Each Locator field stores one reusable way to find TodoMVC content.
  • The constructor configures the input locator plus the item locators.
  • The goto() method opens TodoMVC.
  • The addTodo() method fills the input before pressing Enter.
  • Save pages/todo-page.ts.
  • Confirm the file sidebar lists todo-page.ts inside pages.

Seeing errors in TodoPage?

Check that todo-page.ts sits inside the pages folder. Compare every quote plus every closing brace with the code block.

Help me fix errors in my TodoPage class.

Add completion and filtering actions

The first method covers adding a todo. The second test also needs a named item to be completed before the Completed filter opens.

  • In pages/todo-page.ts, locate the final closing brace beneath addTodo().
  • Place your cursor on the blank line immediately above that brace.
  • Add named completion behavior by pasting this method:
  async completeTodo(text: string) {
    const todo = this.todoItems.filter({ hasText: text });
    await todo.getByRole('checkbox').check();
  }

How does completion work?

  • The text parameter identifies the todo to complete.
  • The filter() call narrows the item locator to the matching text.
  • The check() action completes that item's checkbox.
  • Save pages/todo-page.ts.
  • Check that completeTodo() has no red error underlines.

Is completeTodo underlined?

Make sure the method sits inside the TodoPage class. Check that the locator uses todoItems with the same capitalization as the field.

Help me debug the completeTodo method.

The completion method changes an item's state. A separate method opens the view that displays only completed items.

  • Place your cursor below completeTodo().
  • Add the filter action by pasting this method:
  async showCompleted() {
    await this.page.getByRole('link', { name: 'Completed' }).click();
  }

How does filtering work?

  • The role locator identifies the link whose accessible name is Completed.
  • The click() action switches TodoMVC to its completed-items view.
  • Save pages/todo-page.ts.
  • Check that showCompleted() has no red error underlines.

Is showCompleted underlined?

Check that showCompleted() remains inside the class closing brace. Confirm that the accessible name uses the capitalized value Completed.

Help me debug the showCompleted method.

  • Compare your completed page object with the full file in the second tab.

✔️ Awesome, I've got everything!

Your TodoPage now centralizes every interaction needed by the two tests.

ⓧ I'd like to double check the full code

import type { Locator, Page } from '@playwright/test';

export class TodoPage {
  readonly page: Page;
  readonly newTodoInput: Locator;
  readonly todoItems: Locator;
  readonly todoTitles: Locator;

  constructor(page: Page) {
    this.page = page;
    this.newTodoInput = page.getByPlaceholder('What needs to be done?');
    this.todoItems = page.getByTestId('todo-item');
    this.todoTitles = page.getByTestId('todo-title');
  }

  async goto() {
    await this.page.goto('https://demo.playwright.dev/todomvc/');
  }

  async addTodo(text: string) {
    await this.newTodoInput.fill(text);
    await this.newTodoInput.press('Enter');
  }

  async completeTodo(text: string) {
    const todo = this.todoItems.filter({ hasText: text });
    await todo.getByRole('checkbox').check();
  }

  async showCompleted() {
    await this.page.getByRole('link', { name: 'Completed' }).click();
  }
}
Rewrite the tests around TodoPage

Each test still receives a fresh page fixture in an isolated browser context. The beforeEach hook wraps that page in a new TodoPage before every test.

This setup keeps one test's todo data out of the other test. It also gives both tests the same navigation routine.

  • In tests/todo.spec.ts, select all of the existing code.
  • Replace the selection by pasting this suite:
import { test, expect } from '@playwright/test';
import { TodoPage } from '../pages/todo-page';

test.describe('TodoMVC', () => {
  let todoPage: TodoPage;

  test.beforeEach(async ({ page }) => {
    todoPage = new TodoPage(page);
    await todoPage.goto();
  });

  test('adds one todo item', async () => {
    await todoPage.addTodo('Learn Playwright');

    await expect(todoPage.todoItems).toHaveCount(1);
    await expect(todoPage.todoTitles).toHaveText(['Learn Playwright']);
  });

  test('shows only completed items', async () => {
    await todoPage.addTodo('Write maintainable tests');
    await todoPage.addTodo('Read the HTML report');
    await todoPage.completeTodo('Write maintainable tests');
    await todoPage.showCompleted();

    await expect(todoPage.todoItems).toHaveCount(1);
    await expect(todoPage.todoTitles).toHaveText(['Write maintainable tests']);
  });
});

What changed in the suite?

  • The second import gives the tests access to the shared TodoPage class.
  • The beforeEach hook creates a fresh page object for each isolated page.
  • The first test restores the correct count of 1.
  • The second test completes one named todo before opening the Completed filter.
  • Both tests assert through the page object's shared locators.
  • Save tests/todo.spec.ts.

Before you run the suite, do you expect the old count failure to survive after the tests start using TodoPage?

  • Run the refactored suite from the existing terminal with this command:
npx playwright test

What should you see?

You should see adds one todo item pass under chromium. You should also see shows only completed items pass under the same project.

That is the refactor working. The corrected oracle now passes through the shared page object.

Is a test still failing?

Check that the first test expects a count of 1. Confirm that the TodoPage import points to ../pages/todo-page.

If the filtering test fails, compare every method name with addTodo(), completeTodo(), and showCompleted().

Help me debug the refactored TodoMVC suite.

  • Compare your completed test suite with the full file in the second tab.

✔️ Awesome, I've got everything!

Your two TodoMVC tests now pass through a fresh TodoPage instance for each test.

ⓧ I'd like to double check the full code

import { test, expect } from '@playwright/test';
import { TodoPage } from '../pages/todo-page';

test.describe('TodoMVC', () => {
  let todoPage: TodoPage;

  test.beforeEach(async ({ page }) => {
    todoPage = new TodoPage(page);
    await todoPage.goto();
  });

  test('adds one todo item', async () => {
    await todoPage.addTodo('Learn Playwright');

    await expect(todoPage.todoItems).toHaveCount(1);
    await expect(todoPage.todoTitles).toHaveText(['Learn Playwright']);
  });

  test('shows only completed items', async () => {
    await todoPage.addTodo('Write maintainable tests');
    await todoPage.addTodo('Read the HTML report');
    await todoPage.completeTodo('Write maintainable tests');
    await todoPage.showCompleted();

    await expect(todoPage.todoItems).toHaveCount(1);
    await expect(todoPage.todoTitles).toHaveText(['Write maintainable tests']);
  });
});

Your tests now read like user journeys while TodoPage handles the browser details. Next, you will run the same suite across three browser engines.

Run Cross-Browser Tests

Your shared TodoPage now drives two passing TodoMVC journeys. So far, that evidence comes from one browser engine.

Playwright Test projects apply the same suite to multiple browser presets. This step turns your local checks into browser-labelled results that you can inspect in a report.

In this step, get ready to:
  • Extend the configuration to cover three browser projects.
  • Run the TodoMVC suite across every configured project.
  • Read browser-labelled evidence in an HTML report.
Add Firefox and WebKit projects

Each project entry tells Playwright Test which browser preset should run the suite. Your current entry covers Chromium.

Firefox expands the test matrix with another browser engine. WebKit adds coverage through the engine represented by the Desktop Safari preset.

  • In playwright.config.ts, find the projects array.
  • Locate its current Chromium-only block:
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],

What does the current block do?

This array creates one project named chromium. Every test currently runs with the Desktop Chrome preset.

  • Replace the current projects array with this three-project version:
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],

How do the projects expand coverage?

  • The chromium project uses the Desktop Chrome preset.
  • The firefox project uses the Desktop Firefox preset.
  • The webkit project uses the Desktop Safari preset.
  • Save playwright.config.ts.

✔️ Awesome, I've got everything!

Your configuration now targets all three browser engines.

ⓧ I'd like to double check the full code

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

export default defineConfig({
  testDir: './tests',
  reporter: [['html', { open: 'never' }]],
  use: {
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

What should match?

Compare the imports, reporter setting, trace setting, and three project entries with your saved playwright.config.ts file.

Run the suite in three browsers

The configuration now expands each TodoMVC test across three projects. Before you run the suite, how many passing executions do you expect from two tests?

  • Run the same suite across all configured projects by using this command:
npx playwright test

What does this command do?

  • Playwright reads the three projects from playwright.config.ts.
  • Each project runs both tests from tests/todo.spec.ts.
  • The HTML reporter records the browser-specific results for later review.

You should see six passing executions with chromium, firefox, and webkit project labels. The run also produces an updated HTML report.

Did a browser project fail?

  • Confirm that the Playwright-managed browser downloads from Step 1 are still installed.
  • Compare each project entry in playwright.config.ts with the full configuration above.

Help me diagnose a Playwright test that passes in one browser project but fails in another.

Open and inspect the HTML report

The terminal gives you a quick pass count. The HTML report preserves browser-labelled results with statuses and recorded test steps.

The report command opens the latest report in your browser. Its local viewer keeps the terminal occupied while you inspect the results.

Before you open the report, which three project labels do you expect to find?

  • Open the latest HTML report by running this command:
npx playwright show-report

What does this command do?

This command opens the most recent Playwright HTML report. The report connects each result to its project, status, and recorded steps.

  • Filter the report by project to compare the browser-labelled results.
  • Filter the report by status to confirm the tests passed.
  • Open one TodoMVC test result.
  • Review its recorded steps.

You should see passing results labelled for Chromium, Firefox, and WebKit. Each opened result shows the steps from its TodoMVC journey.

Strong work. Your two user journeys now carry reusable evidence from three browser engines.

Report not opening?

  • Confirm that the preceding test run completed and generated an HTML report.
  • Check the terminal for a message describing why the local report viewer could not start.

Help me troubleshoot why my latest Playwright HTML report is not opening.

Secret mission

Add Data-Driven Test Coverage

Your baseline suite proves two journeys across three browsers. Extend it with a small data table that creates one independent test for every todo name without copying the test body.

Clean Up Your Resources

Clean Up Your Resources

This local project has no ongoing cost. Choose whether to keep its resources, pause your work, or delete everything.

Resources you used:

  • The local todo-playwright-tests project folder with your TypeScript source files.
  • The generated node_modules dependency directory.
  • The generated package-lock.json lock file.
  • Local Playwright test result files.
  • The generated HTML report.
  • Playwright-managed Chromium browser binaries.
  • Playwright-managed Firefox browser binaries.
  • Playwright-managed WebKit browser binaries.

Keep everything running

No action is needed. Choose this option if you plan to extend the test suite or demonstrate it again.

  • Your todo-playwright-tests folder remains ready for future test runs.
  • Your browser binaries remain installed for future test runs.
  • Your latest test results remain available locally.
  • Your HTML report remains available for reviewing browser-labelled results.
  • These local resources create no ongoing cost.

Pause - I'll come back to this later

Pausing closes your local work while preserving every project resource.

  • Close Playwright UI Mode if it is still running.
  • Close the HTML report browser tab.
  • Close the terminal window that is currently in todo-playwright-tests.
  • Keep the todo-playwright-tests folder for your next session.
  • Keep the browser binaries for your next test run.

No cloud service continues running after you close these local windows.

Delete - I don't want to use this again

Remove the browser binaries first. Delete the project folder afterwards.

  • Remove the browser binaries associated with this Playwright installation by running:
npx playwright uninstall

What Does This Command Remove?

This cleanup command removes Chromium, Firefox, and WebKit for the Playwright installation associated with this project.

Browser Cleanup Did Not Finish?

Return to the terminal that remains inside todo-playwright-tests before retrying the cleanup.

Help me diagnose why the Playwright browser cleanup did not finish.

Protect Other Playwright Projects

The command above targets browser binaries for the current Playwright installation. A broader cleanup affects every Playwright installation on your Mac.

  • Remove browser binaries for every Playwright installation only when that is your goal by running:
npx playwright uninstall --all

This broader command removes browser binaries used by every Playwright installation on your Mac.

Removing the project folder is permanent. The next command names only todo-playwright-tests.

  • Leave the project directory before deleting the local folder by running:
cd ..
rm -rf todo-playwright-tests

What Do These Commands Do?

The first line moves your terminal to the folder that contains the project. The second line recursively deletes the project folder plus everything stored inside it.

Still See the Project Folder?

Check that your terminal moved one level above todo-playwright-tests before retrying the deletion.

Help me safely remove the remaining project folder.

  • Return to the parent folder in Finder.
  • Confirm that todo-playwright-tests no longer appears there.

That completes the cleanup. Your project folder plus its Playwright-managed browser binaries are removed.

Nice Work!

Nice Work!

You did it! You've built a maintainable TodoMVC test suite with Playwright Test.

What you learned:

  • Built TypeScript browser tests that add todos. Extended the suite to complete items. Verified that the Completed filter shows only finished items.
  • Diagnosed an intentional assertion failure in UI Mode. Used the failure evidence to explain why one created item could not satisfy an expected count of two.
  • Refactored repeated interactions with the Page Object Model. Ran the isolated suite in Chromium. Repeated the checks in Firefox. Completed the cross-browser run in WebKit. Used the HTML report to compare browser-labelled results.
  • Secret Mission: Generated independent data-driven tests from the todoNames array. Used a for...of loop with a template literal to cover each todo name without copying the test body.

Ready to quiz yourself?