Automate a Todo App with Playwright

Build a 5-test Playwright suite that automates a todo app end-to-end.

Introduction

30 Second Summary

Think about how many times you have clicked through a website to check if a button still works or a form still submits. Now imagine a script that does all of that clicking for you in seconds, every single time you make a change.

In this project, you will build an automated test suite using Playwright that opens a real browser, interacts with a todo app, and verifies everything works correctly.

What You'll Build

You will run a single command and watch a browser spring to life, add items to a todo list, mark them complete, filter the results, and report back whether everything passed.

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

  • A 5-test automation suite that adds todos, completes them, checks the item count, and filters between active and completed items.
  • A recorded test generated by Codegen, where Playwright watches you use a website and writes the test code for you.
  • A clean test structure using hooks to share setup logic across all your tests so your suite stays maintainable as it grows.
  • A professional HTML test report dashboard showing which tests passed across your suite.
  • Secret Mission: Run your tests across Chromium, Firefox, and WebKit simultaneously and explore the cross-browser HTML report.

Are there any prerequisites?

You will need VS Code and Node.js installed on your computer. Step 1 walks you through installing Playwright itself, so no prior testing experience is needed.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Playwright

To automate a real web browser, you need a testing framework that can launch browsers, click buttons, fill forms, and verify results on your behalf.

Playwright installs everything you need in a single command. That includes the test runner, TypeScript support, and actual browser binaries for Chromium, Firefox, and WebKit.

In this step, get ready to:
  • Install Playwright and download browser binaries.
  • Explore the generated project structure and simplify the config.
  • Run the example test in headed mode and watch a browser open automatically.
Open VS Code and create your project folder
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open the built-in terminal by pressing Ctrl+` (backtick key, top-left of your keyboard). A terminal panel appears at the bottom of VS Code.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir playwright-todo-tests
cd playwright-todo-tests
  • Confirm the folder was created by running:
pwd

You should see a path ending in playwright-todo-tests. This is where all your test files will live.

Install Playwright with TypeScript support

Playwright provides a single scaffold command that installs the test framework, creates starter files, and downloads real browser binaries.

  • Install Playwright by running this command:
npm init playwright@latest

What will this command ask you?

The installer asks four questions. Answer them one at a time:

  • TypeScript or JavaScript? Press Enter to accept TypeScript (the default).
  • Where to put your end-to-end tests? Press Enter to accept the default tests folder.
  • Add a GitHub Actions workflow? Type false and press Enter to skip this.
  • Install Playwright browsers? Press Enter to accept yes (the default). This downloads Chromium, Firefox, and WebKit.

The browser download takes a minute or two. When it finishes, you will see a success message in your terminal.

  • Confirm the scaffold files exist by running:
ls

You should see playwright.config.ts, package.json, package-lock.json, and a tests folder in the output.

What are these files?

  • playwright.config.ts controls how your tests run: which browsers, how many retries, and what reports to generate.
  • tests/example.spec.ts is a sample test that Playwright includes so you can verify everything works right away.
  • package.json tracks your project dependencies, including the @playwright/test package.

Seeing permission errors or network timeouts?

  • On macOS, if you see a permission error, try running the command with sudo in front.
  • If the browser download times out, check your internet connection and run npx playwright install to retry just the browser download.

Still stuck? Help me troubleshoot the Playwright installation

Simplify the config to use only Chromium

The default config includes projects for multiple browsers. For now, you will simplify it to just Chromium so your test output stays clean and focused.

  • In the VS Code Explorer sidebar on the left, click playwright.config.ts to open it.
  • Select all the content in the file by pressing Cmd+A (macOS) or Ctrl+A (Windows).
  • Replace it with the following code:
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

What does this config do?

  • testDir: './tests' tells Playwright where to find your test files.
  • fullyParallel: true runs all tests at the same time for speed.
  • reporter: 'html' generates a rich HTML dashboard after each test run.
  • projects lists which browsers to test in. You have just Chromium for now. The Secret Mission adds more browsers later.
  • Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Double check you have saved playwright.config.ts and move on to running your first test.

ⓧ I'd like to double check the full code

Your playwright.config.ts should look exactly like this:

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

File not showing in the sidebar?

  • Make sure VS Code has the playwright-todo-tests folder open. Click File then Open Folder and select the playwright-todo-tests folder on your Desktop.

Need more help? Help me open my project folder in VS Code

Run the example test in headed mode

Playwright includes a sample test that visits a real website. Running it in headed mode opens a visible browser window so you can watch the automation happen in real time.

Before you run this, what do you think will happen? Will a browser window appear and stay open, or will it close on its own?

  • Run the example test in headed mode by running this command:
npx playwright test --headed --project=chromium

A Chromium browser window opens, navigates to a website, and closes automatically within a few seconds. In your terminal you will see output confirming the tests passed with a summary showing all tests passed.

What just happened?

Playwright launched a real Chromium browser, ran the code in tests/example.spec.ts, verified the pages loaded correctly, and then closed the browser. All automatically.

The --headed flag made the browser visible. Without it, Playwright runs headless (no window appears). The --project=chromium flag told Playwright to use only the Chromium browser from your config.

Browser did not open or tests failed?

  • If you see a message about browsers not being installed, run npx playwright install and then try the test command again.
  • If the test fails with a network error, check that you have an active internet connection. The example test visits an external website.
  • On macOS, if your system blocks the browser from opening, go to System Settings then Privacy & Security and allow the Chromium binary.

Still stuck? Help me get the Playwright example test running

Your environment is ready. Playwright is installed, the browsers are downloaded, and you have seen a test run with your own eyes. Next up, you will use Codegen to record browser interactions and have Playwright write test code for you automatically.

Record Your First Test with Codegen

Your Playwright setup is ready and you have already seen a browser open and close automatically. Now it is time to create your own test.

Writing test code from scratch is intimidating when you have never done it before. Playwright solves this with Codegen, a tool that records your browser interactions and writes the test code for you.

In this step, get ready to:
  • Launch Codegen and interact with a real todo app in the browser.
  • Copy the auto-generated test code from the Playwright Inspector.
  • Run the recorded test and watch the browser replay your actions.
Launch Codegen pointed at the TodoMVC demo app

Codegen opens two windows at once: a browser where you interact with a website normally, and the Playwright Inspector that watches and writes code as you go.

  • Launch Codegen by running this command in your terminal:
npx playwright codegen demo.playwright.dev/todomvc

Two windows appear on your screen. The browser window shows the TodoMVC demo app. The Playwright Inspector window shows an empty code panel, ready to record.

What are these two windows?

The browser window is where you use the website. Every click and keystroke you make here gets translated into test code.

The Playwright Inspector is where that generated code appears in real time. It is a live transcript of your actions.

  • Click the text input at the top of the todo app (it says What needs to be done?).
  • Type Buy groceries and press Enter.
  • Look at the Playwright Inspector window. You should see new lines of code appearing that match the action you just performed.

Don't see two windows?

The Inspector may open behind the browser window. Check your taskbar or use Alt+Tab (Windows) or Cmd+Tab (macOS) to find it.

If you see an error about browsers not being installed, run npx playwright install first, then try the codegen command again.

Help me troubleshoot

Record a few interactions

Now that you can see code being generated, add a few more actions so your recorded test does something meaningful.

  • Add a second todo item by typing Walk the dog in the input and pressing Enter.
  • Add a third todo item by typing Read a book and pressing Enter.
  • Check off the first item (Buy groceries) by clicking the circle checkbox to its left.
  • Watch the Inspector window after each action. Notice how Codegen chooses locators like getByPlaceholder, getByRole, and getByText to identify elements.

What are locators?

Locators are how Playwright finds elements on a page. Instead of fragile CSS selectors, Codegen prioritizes human-readable locators based on role, text, and placeholder attributes.

This means your tests stay stable even if the app's internal HTML structure changes.

  • Click the copy button in the Inspector toolbar to copy all the generated code to your clipboard.
  • Close both the browser window and the Inspector window.

You now have a complete test script on your clipboard, written entirely by Codegen.

Save and run the recorded test

The generated code is on your clipboard. Now you will save it as a test file and run it to confirm Playwright can replay your actions.

  • Switch back to VS Code.
  • In the Explorer sidebar, right-click the tests folder and select New File.
  • Name the file recorded.spec.ts and press Enter.
  • Paste the copied code into recorded.spec.ts using Cmd+V (macOS) or Ctrl+V (Windows).
  • Save the file using Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm the file exists by running:
ls tests/

You should see recorded.spec.ts alongside example.spec.ts in the output.

Before you run this test, what do you think will happen in the browser? Will it look different from the example test you ran in Step 1?

  • Run your recorded test in headed mode by running this command:
npx playwright test tests/recorded.spec.ts --headed --project=chromium

A browser window opens and replays every action you recorded. You should see todo items being typed, submitted, and checked off automatically.

What just happened?

Playwright read the code that Codegen generated and executed each action in a real browser. The same clicks and keystrokes you performed manually are now automated and repeatable.

Every time you run this command, the browser performs the same sequence of actions. This is the foundation of end-to-end testing.

Test failing or browser not opening?

Make sure you saved recorded.spec.ts before running the command.

If you see a syntax error, the clipboard might not have copied cleanly. Close the Inspector window, run the codegen command again, and re-record a few actions.

If the test passes but finishes too fast to see, the actions are correct. Codegen-generated tests run quickly because Playwright auto-waits for elements without adding pauses.

Help me debug my recorded test

You just went from clicking around a website to having a fully automated test, without writing a single line of code yourself. Next up, you will write tests from scratch to understand exactly how the code works.

Write Your First Test From Scratch

Codegen recorded your interactions perfectly. But what happens when you need to test a scenario you cannot click through manually, like verifying specific text or testing an edge case?

Understanding the code yourself means you can write tests for any scenario. In this step, you'll hand-write three tests that navigate to the TodoMVC app, add todo items, and verify they appear on the page.

In this step, get ready to:
  • Write a test that checks the page title loads correctly.
  • Write a test that adds a single todo and verifies it appears.
  • Write a test that adds multiple todos and verifies both appear.
Create the test file and write a title check

Every Playwright test file starts with an import and one or more test functions. Each test function receives a page object that controls the browser.

  • In VS Code, create a new file called tests/todo-app.spec.ts inside your playwright-todo-tests project.
  • Add the following code to write your first test:
import { test, expect } from '@playwright/test';

// Verify the TodoMVC page loads with the expected title
test('page has correct title', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
  await expect(page).toHaveTitle(/TodoMVC/);
});

What does this code do?

  • import { test, expect } brings in the two tools you need: test to define a test, and expect to make assertions.
  • async ({ page }) gives you a fresh browser page for each test. Playwright handles opening and closing the browser automatically.
  • page.goto() navigates the browser to the TodoMVC app.
  • toHaveTitle(/TodoMVC/) checks that the page title contains "TodoMVC". The slashes make it a regex pattern, so it matches any title containing that text.
  • Save tests/todo-app.spec.ts.
  • Run your new test by entering this command in the terminal:
npx playwright test tests/todo-app.spec.ts --project=chromium

You should see 1 passed in the terminal output.

Seeing a failure instead?

  • Check that the file is saved in the tests/ folder, not the project root.
  • Make sure the import line matches exactly: import { test, expect } from '@playwright/test';.
  • Confirm your URL is https://demo.playwright.dev/todomvc/ with the trailing slash.

Help me debug my failing Playwright test

Add a test that creates a todo

Your first test just checks the page loads. Now you'll interact with the app by typing into the input field and pressing Enter to add a todo item.

  • In tests/todo-app.spec.ts, add this second test below the closing }); of your first test:
test('can add a new todo', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
  // Find the input by its placeholder text, type a todo, and submit
  await page.getByPlaceholder('What needs to be done?').fill('Buy groceries');
  await page.getByPlaceholder('What needs to be done?').press('Enter');
  // Verify the todo item now appears on the page
  await expect(page.getByText('Buy groceries')).toBeVisible();
});

What does this code do?

  • getByPlaceholder('What needs to be done?') finds the input field by its placeholder text. This is the same locator strategy Codegen uses.
  • .fill('Buy groceries') types text into the input, just like a real user would.
  • .press('Enter') submits the todo by pressing the Enter key.
  • toBeVisible() waits for the element to appear and asserts it is visible. Playwright automatically retries the assertion until the element shows up or the timeout expires.
  • Save tests/todo-app.spec.ts.
  • Run the tests again to confirm both pass:
npx playwright test tests/todo-app.spec.ts --project=chromium

You should see 2 passed in the terminal output.

Only 1 test passing?

  • Make sure the second test is outside the closing }); of the first test, not nested inside it.
  • Check that the placeholder text matches exactly: 'What needs to be done?' with the question mark.

Help me figure out why my second test is failing

Add a test that creates multiple todos

When you repeat the same locator multiple times, you can store it in a variable. This keeps your test cleaner and easier to read.

  • In tests/todo-app.spec.ts, add this third test below the closing }); of your second test:
test('can add multiple todos', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
  // Store the input locator in a variable to avoid repeating it
  const input = page.getByPlaceholder('What needs to be done?');
  await input.fill('Buy groceries');
  await input.press('Enter');
  await input.fill('Walk the dog');
  await input.press('Enter');
  // Verify both todos appear on the page
  await expect(page.getByText('Buy groceries')).toBeVisible();
  await expect(page.getByText('Walk the dog')).toBeVisible();
});

What does this code do?

  • const input = page.getByPlaceholder('What needs to be done?') stores the locator in a variable called input. You can reuse it for every todo you add.
  • The test adds two items sequentially, then asserts both are visible. Playwright's auto-waiting ensures each fill and press completes before the next action runs.
  • Save tests/todo-app.spec.ts.

Before you run the full suite, how many tests do you expect to pass?

  • Run the complete test file in headed mode to watch all three tests execute:
npx playwright test tests/todo-app.spec.ts --headed --project=chromium

You should see a browser open three separate times (once per test) and 3 passed in the terminal. Each test gets its own fresh browser context, so they never interfere with each other.

Seeing fewer than 3 tests pass?

  • Make sure each test function starts with test( at the top level of the file, not nested inside another test.
  • Check that every await keyword is present. Missing await causes Playwright actions to run out of order.
  • Verify you have the correct closing braces. Each test ends with }); on its own line.

Help me debug my Playwright test suite

✔️ Awesome, I've got everything!

Great. Double check you have saved tests/todo-app.spec.ts and all 3 tests pass.

ⓧ I'd like to double check the full code

Here is the complete tests/todo-app.spec.ts file. Compare yours against it:

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

test('page has correct title', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
  await expect(page).toHaveTitle(/TodoMVC/);
});

test('can add a new todo', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
  await page.getByPlaceholder('What needs to be done?').fill('Buy groceries');
  await page.getByPlaceholder('What needs to be done?').press('Enter');
  await expect(page.getByText('Buy groceries')).toBeVisible();
});

test('can add multiple todos', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
  const input = page.getByPlaceholder('What needs to be done?');
  await input.fill('Buy groceries');
  await input.press('Enter');
  await input.fill('Walk the dog');
  await input.press('Enter');
  await expect(page.getByText('Buy groceries')).toBeVisible();
  await expect(page.getByText('Walk the dog')).toBeVisible();
});

You now have three hand-written tests that prove the TodoMVC app works. Notice how each test repeats the same page.goto() line. In the next step, you'll clean that up with hooks so the navigation lives in one place.

Organize Tests with Hooks

Your three tests all pass, each one verifying a different interaction with the todo app.

But every test starts with the exact same page.goto line. If the app URL ever changes, you would need to update all three tests manually. This step extracts the repeated navigation into a shared hook so it lives in one place.

In this step, get ready to:
  • Spot the duplicated navigation code across your tests.
  • Extract it into a shared test.beforeEach hook.
  • Confirm all 3 tests still pass with the repetition removed.
Spot the repeated navigation code
  • Switch back to tests/todo-app.spec.ts in VS Code.
  • Look at the first line inside each of your three test functions.

Every single test opens with this line:

await page.goto('https://demo.playwright.dev/todomvc/');

Three copies of the same URL string means three places to update if the address changes. That is a maintenance problem waiting to happen.

Why is repetition a problem in tests?

Duplicated setup code makes your suite fragile. One typo in a single copy creates a confusing failure that looks like an app bug. Extracting shared logic into a hook keeps your suite consistent and easy to update.

Wrap tests in describe and add beforeEach

Playwright provides test.describe to group related tests and test.beforeEach to run shared setup before every test in the group.

  • Select all the code in tests/todo-app.spec.ts and replace it with this refactored version:
import { test, expect } from '@playwright/test';

// Group all todo app tests under one describe block
test.describe('Todo App', () => {
  // Navigate to the app before each test runs
  test.beforeEach(async ({ page }) => {
    await page.goto('https://demo.playwright.dev/todomvc/');
  });

  test('page has correct title', async ({ page }) => {
    await expect(page).toHaveTitle(/TodoMVC/);
  });

  test('can add a new todo', async ({ page }) => {
    await page.getByPlaceholder('What needs to be done?').fill('Buy groceries');
    await page.getByPlaceholder('What needs to be done?').press('Enter');
    await expect(page.getByText('Buy groceries')).toBeVisible();
  });

  test('can add multiple todos', async ({ page }) => {
    const input = page.getByPlaceholder('What needs to be done?');
    await input.fill('Buy groceries');
    await input.press('Enter');
    await input.fill('Walk the dog');
    await input.press('Enter');
    await expect(page.getByText('Buy groceries')).toBeVisible();
    await expect(page.getByText('Walk the dog')).toBeVisible();
  });
});

What changed in this refactoring?

  • test.describe('Todo App', () => { ... }) wraps all three tests into a named group. This keeps related tests organized together.
  • test.beforeEach runs the page.goto navigation automatically before each test executes. No test needs its own goto anymore.
  • Each individual test now starts directly with its unique logic. The shared setup lives in exactly one place.
  • Save tests/todo-app.spec.ts.

Seeing red squiggly lines in VS Code?

Make sure every opening brace { and parenthesis ( has a matching closing pair. The test.describe block wraps everything, so the final }); at the bottom closes it.

help me fix a syntax error in my Playwright test.describe block

Run the refactored suite

The tests should behave exactly the same as before. The only difference is where the navigation lives.

  • Before you run this, predict: will all 3 tests still pass, or did the refactoring break something?
  • Run your test suite by running this command:
npx playwright test tests/todo-app.spec.ts --project=chromium

You should see all 3 tests pass with a summary line showing 3 passed.

Why run without --headed this time?

You already know the tests work visually from Step 3. Running headless (without the browser window) is faster and confirms the logic still holds after your refactoring. This is how you will typically run tests during development.

Tests failing after the refactoring?

Check that you removed the await page.goto(...) line from inside each individual test. If a test still has its own goto, the page navigates twice and timing can get unpredictable.

help me debug why my Playwright tests fail after adding test.beforeEach

✔️ Awesome, I've got everything!

Great. Make sure you have saved tests/todo-app.spec.ts with the refactored code.

ⓧ I'd like to double check the full code

Here is the complete tests/todo-app.spec.ts file after the refactoring:

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

test.describe('Todo App', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://demo.playwright.dev/todomvc/');
  });

  test('page has correct title', async ({ page }) => {
    await expect(page).toHaveTitle(/TodoMVC/);
  });

  test('can add a new todo', async ({ page }) => {
    await page.getByPlaceholder('What needs to be done?').fill('Buy groceries');
    await page.getByPlaceholder('What needs to be done?').press('Enter');
    await expect(page.getByText('Buy groceries')).toBeVisible();
  });

  test('can add multiple todos', async ({ page }) => {
    const input = page.getByPlaceholder('What needs to be done?');
    await input.fill('Buy groceries');
    await input.press('Enter');
    await input.fill('Walk the dog');
    await input.press('Enter');
    await expect(page.getByText('Buy groceries')).toBeVisible();
    await expect(page.getByText('Walk the dog')).toBeVisible();
  });
});

Your suite is clean and easy to maintain. The navigation lives in one place, and adding new tests no longer means copying that URL. Next up, you will write tests for real user interactions like completing todos and filtering the list.

Test Real User Interactions

Your tests are organized and your navigation lives in one place. The suite is clean and ready to grow.

Right now your tests only cover adding items to the list. Real users do more than that. They complete todos, check how many are left, and filter their view. Your suite needs to cover these critical paths to be useful as a safety net.

In this step, get ready to:
  • Write a test that completes a todo and verifies the item count updates.
  • Write a test that filters between active and completed todos.
  • Run the full suite and confirm all 5 tests pass.
Write a test that completes a todo and checks the count

When a user checks off a todo item, the app updates a counter showing how many items remain. This test verifies that interaction works correctly.

  • In tests/todo-app.spec.ts, add this test after your existing 'can add multiple todos' test, inside the test.describe block:
  test('can complete a todo and count updates', async ({ page }) => {
    // Add two todos to the list
    const input = page.getByPlaceholder('What needs to be done?');
    await input.fill('Buy groceries');
    await input.press('Enter');
    await input.fill('Walk the dog');
    await input.press('Enter');
    // Verify the counter shows both items
    await expect(page.getByText('2 items left')).toBeVisible();
    // Check off the first todo using its checkbox
    await page.locator('.todo-list li').filter({ hasText: 'Buy groceries' }).getByRole('checkbox').check();
    // Verify the count decreased by one
    await expect(page.getByText('1 item left')).toBeVisible();
  });

What does this code do?

This test adds two items, then verifies the counter at the bottom of the app reads 2 items left.

The page.locator('.todo-list li').filter({ hasText: 'Buy groceries' }) line narrows down to the specific list item containing that text. Then .getByRole('checkbox').check() ticks its checkbox. After checking it off, the count should drop to 1 item left.

  • Save tests/todo-app.spec.ts.
  • Run the updated suite by running this command:
npx playwright test tests/todo-app.spec.ts --headed --project=chromium
  • Watch the browser open and replay all four tests. The new test adds two todos, checks one off, and the counter changes from 2 items left to 1 item left.

Test failing with a timeout?

Make sure your new test is inside the closing }); of the test.describe block, not after it.

Check that you typed 2 items left and 1 item left exactly (no capital letters, no extra spaces).

Still stuck? Help me debug why my Playwright completion test is timing out.

Write a test that filters active and completed todos

The TodoMVC app has filter links at the bottom: All, Active, and Completed. This test verifies that clicking those filters shows and hides the correct items.

  • Add this test below the one you just wrote, still inside the test.describe block:
  test('can filter active and completed todos', async ({ page }) => {
    // Add two todos and complete one
    const input = page.getByPlaceholder('What needs to be done?');
    await input.fill('Buy groceries');
    await input.press('Enter');
    await input.fill('Walk the dog');
    await input.press('Enter');
    await page.locator('.todo-list li').filter({ hasText: 'Buy groceries' }).getByRole('checkbox').check();
    // Click Active filter and verify only uncompleted todo shows
    await page.getByRole('link', { name: 'Active' }).click();
    await expect(page.getByText('Walk the dog')).toBeVisible();
    await expect(page.getByText('Buy groceries')).not.toBeVisible();
    // Click Completed filter and verify only completed todo shows
    await page.getByRole('link', { name: 'Completed' }).click();
    await expect(page.getByText('Buy groceries')).toBeVisible();
    await expect(page.getByText('Walk the dog')).not.toBeVisible();
  });

What does this code do?

This test sets up a scenario where one todo is completed and one is still active. Then it exercises both filter views.

  • page.getByRole('link', { name: 'Active' }) finds the filter link by its accessible role and visible text.
  • expect(...).not.toBeVisible() asserts that an item is hidden from view. This confirms the filter is actually hiding completed items, not just rearranging them.
  • Save tests/todo-app.spec.ts.

Seeing a syntax error?

Make sure the closing }); for this test appears before the final }); that closes the test.describe block. Every test function needs its own closing brace, parenthesis, and semicolon.

Need help? Help me fix a syntax error in my Playwright test file.

Run the complete suite

You now have 5 tests covering title verification, adding single and multiple todos, completing a todo, and filtering. Time to see them all pass together.

  • Before you run, predict: how many tests do you expect to pass? Do you think the filter test will work on the first try?
  • Run the full suite by running this command:
npx playwright test tests/todo-app.spec.ts --project=chromium

You should see 5 passed in the terminal output. All five tests pass in just a few seconds.

Why no --headed flag this time?

Running without --headed keeps the browser invisible (headless mode). The tests still execute in a real browser engine, but they finish faster because there is no window to render. This is how you would run tests in a real CI/CD pipeline.

✔️ Awesome, I've got everything!

All 5 tests passing means your suite covers adding, completing, and filtering todos. Make sure you have saved tests/todo-app.spec.ts.

ⓧ I'd like to double check the full code

Your tests/todo-app.spec.ts file should contain these sections in order:

  • The import line: import { test, expect } from '@playwright/test'.
  • A test.describe('Todo App', ...) wrapper around everything.
  • A test.beforeEach that navigates to the TodoMVC app.
  • Test 1: 'page has correct title'.
  • Test 2: 'can add a new todo'.
  • Test 3: 'can add multiple todos'.
  • Test 4: 'can complete a todo and count updates'.
  • Test 5: 'can filter active and completed todos'.

Scroll up and confirm each chunk matches what you pasted. Then re-run the test command to verify all 5 pass.

Your suite now covers real user workflows. You can add items, complete them, and filter the list. Next up, take on the Secret Mission and run these tests across multiple browsers at once.

Secret mission

Cross-Browser Testing and HTML Reports

Your tests only run in Chromium right now. Real users visit with Firefox and Safari too. Can you run your suite across all three browsers and explore the cross-browser HTML report dashboard?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The playwright-todo-tests project folder on your Desktop.
  • Playwright browser binaries (Chromium, Firefox, WebKit) downloaded during setup.

Keep everything running

No action needed. Choose this if you want to keep writing more tests or experimenting with Playwright.

  • Your playwright-todo-tests folder stays on your Desktop, ready to open any time.
  • Playwright browsers remain installed and available for future test runs.

Pause - I'll come back to this later

Free up disk space by removing the downloaded browser binaries. Your test files and configuration stay intact so you can pick up where you left off.

  • Remove the Playwright browser binaries by running this command inside your project folder:
npx playwright uninstall
  • Your test files, config, and node_modules stay in place.
  • When you come back, re-install browsers by running npx playwright install from inside the project folder.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your test files, configuration, and the Playwright browsers.

  • First, remove the Playwright browser binaries by running:
npx playwright uninstall
  • Then, navigate to your Desktop and delete the project folder.

On macOS or Linux:

cd ~/Desktop
rm -rf playwright-todo-tests

On Windows:

cd ~\Desktop
rmdir /s /q playwright-todo-tests
  • Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows) on your Desktop. You should no longer see playwright-todo-tests in the output.

Nice Work!

Nice Work!

You just built a complete end-to-end test suite that automates a real web application. Your browser opens, clicks, types, and verifies results all on its own.

You've learned how to:

  • Set up Playwright with TypeScript and used Codegen to record browser interactions and auto-generate test code.
  • Written 5 tests from scratch covering adding todos, completing them, checking counts, and filtering between active and completed items.
  • Organized tests with test.describe and test.beforeEach to eliminate repetition and keep your suite maintainable as it grows.
  • Secret Mission: ran tests across Chromium, Firefox, and WebKit simultaneously and explored the HTML report dashboard showing cross-browser results.

Ready to quiz yourself?