E2E Test Suite with Playwright

Build an automated test suite for an online store using Playwright and TypeScript.

Introduction

30 Second Summary

Every time you shop online, automated end-to-end tests run behind the scenes to make sure every button and checkout step works correctly. They catch bugs before real users ever see them.

In this project, you will build an end-to-end test suite that automates a user's journey through an online store using Playwright and TypeScript. You will also set up a CI/CD pipeline with GitHub Actions to run your tests automatically on every code push.

What You'll Build

A Playwright test suite that tests login, product browsing, and shopping cart flows on the Swag Labs demo store across multiple browsers.

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

  • A Playwright project with TypeScript, configured for cross-browser testing and HTML reporting.
  • Page Object Model classes for Login, Inventory, and Cart pages that keep your tests maintainable.
  • A GitHub Actions pipeline that runs your test suite on every push and publishes test reports as downloadable artifacts.
  • Secret Mission: Write tests for problem user accounts to practice negative testing and visual defect detection.

Are there any prerequisites?

This project assumes basic familiarity with JavaScript or TypeScript syntax, including async/await and classes. Some exposure to browser-based testing concepts is helpful but not required.

Before We Start

Before We Start...

Set Up Your Playwright Project

Before you can write a single test, you need a project wired up with the right tools. Playwright ships with its own scaffolding command that generates everything you need in one shot: a config file, a test runner, browser binaries, and even a GitHub Actions workflow.

In this step, you will scaffold a new TypeScript project, install the Playwright VS Code extension, and run the example tests to confirm everything works.

In this step, get ready to:
  • Scaffold a Playwright project with TypeScript and browser binaries.
  • Install the Playwright Test extension in VS Code.
  • Run the example tests and view the HTML report.
Create your project directory and initialize Playwright

Playwright's init command creates a complete project structure, downloads browser engines, and optionally generates a CI/CD workflow file. You will run it inside a fresh directory on your Desktop.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir playwright-ecommerce
cd playwright-ecommerce
  • Initialize a new Playwright project by running this command:
npm init playwright@latest

What does this command do?

npm init playwright@latest runs Playwright's project scaffolding wizard. It creates your config file, a sample test, installs the @playwright/test package, and downloads the browser engines Playwright uses to run tests.

The command launches an interactive wizard that asks a series of questions. Respond to each prompt one at a time:

  • When asked whether to use TypeScript or JavaScript, select TypeScript (the default). Press Enter.
  • When asked where to put your end-to-end tests, accept the default tests folder. Press Enter.
  • When asked to add a GitHub Actions workflow, type true or press Enter if it defaults to true.
  • When asked to install Playwright browsers, confirm yes. This downloads Chromium, Firefox, and WebKit binaries. It may take a minute or two.
  • Confirm the scaffold created your project files by running:
ls

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

Command not found or install failed?

If you see a command not found error for npm, Node.js is not installed or not on your PATH. Playwright requires Node.js 20.x, 22.x, or 24.x.

If the browser download stalls, check your internet connection and try running the command again.

Help me troubleshoot the Playwright init command

Open the project in VS Code and install the Playwright extension
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the playwright-ecommerce folder on your Desktop, then click Open.

Playwright has an official VS Code extension that lets you run and debug tests directly from the editor. It adds a test explorer panel and inline run buttons next to each test.

  • Click the Extensions icon in the left sidebar. You can also press Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type Playwright Test for VS Code in the search bar.
  • Find the extension by Microsoft (extension ID: ms-playwright.playwright) and click Install.

Now take a look at the files the scaffold created. In the Explorer sidebar (click the top icon or press Cmd+Shift+E / Ctrl+Shift+E), you will see these key files:

  • Click on playwright.config.ts to open it. This is the central configuration file for your test suite.
  • Click on tests/example.spec.ts. This is the sample test that Playwright generated for you.
  • Click on package.json. Notice that @playwright/test is listed as a dev dependency.

What is playwright.config.ts?

This file controls how your tests run. It sets the test directory (./tests), enables parallel execution, configures the HTML reporter, and defines which browsers to test against.

Right now, the projects array only includes Chromium. You will add more browsers in a later step.

Why Playwright instead of Selenium?

Selenium uses HTTP round-trips for every action (click, read text, check element), which creates race condition windows and requires manual waits. Playwright uses a persistent WebSocket connection that hooks directly into the browser's event loop. The result is 2-3x faster execution and significantly less flakiness.

Playwright also bundles its own test runner, assertions, HTML reporter, and VS Code extension. With Selenium, you would need to assemble all of those from separate tools.

Run the example tests and view the report

The scaffolded project includes a sample test file that visits the Playwright documentation site and checks a few things on the page. Running it confirms that your browsers, test runner, and reporter are all working.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (same shortcut on macOS and Windows).
  • Run the example tests by entering this command:
npx playwright test

You should see output showing 2 tests passed, with a green checkmark next to each. The tests run in headless mode by default, so no browser window opens.

What is headless mode?

Headless mode runs the browser without a visible window. The browser still loads pages, clicks buttons, and reads text. It just does it all in the background. This is faster and is the default for CI environments where no screen is available.

Tests failed or browsers not found?

If you see an error about missing browsers, the download during init may not have completed. Install them manually by running:

npx playwright install --with-deps

If the tests fail with a network error, make sure you have an internet connection. The example tests visit playwright.dev and need web access.

Help me debug my Playwright test failures

Playwright generates an HTML report after every test run. This report shows which tests passed or failed, how long each took, and lets you drill into individual test steps.

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

A browser tab opens with the Playwright HTML report. You should see both example tests listed with green pass indicators and their execution times.

  • Press Ctrl+C in your terminal to stop the report server when you are done viewing.

Finally, try running the tests in headed mode so you can watch the browser open and the test actions happen in real time.

  • Run the tests in headed mode by entering this command:
npx playwright test --headed

A Chromium browser window pops open, navigates to the Playwright docs, and closes after the tests finish. This is useful for debugging when a test behaves unexpectedly.

Browser opens but closes instantly?

That is expected. The example tests are fast, so the browser appears and disappears in about a second. You will get a better look when you write longer tests in the next step.

Your Playwright project is scaffolded, your extension is installed, and your test runner is confirmed working. Next up, you will delete the example tests and write your first real login test against the Swag Labs demo store.

Write Your First Login Test

Your Playwright project is scaffolded and ready. You confirmed the example tests pass and explored the HTML report.

Now it's time to write real tests against an actual website. You'll automate the login flow on Swag Labs (saucedemo.com), an e-commerce demo site purpose-built for testing practice.

In this step, get ready to:
  • Delete the example test file and create a login test file.
  • Write a test that logs in with valid credentials.
  • Write a test that verifies a locked-out user sees an error message.
Delete the example test and create your login file

The scaffolded example.spec.ts file tests the Playwright docs website. You don't need it anymore. You'll replace it with a test file that targets the Swag Labs login page.

  • Delete the example test file by running this command:
rm tests/example.spec.ts
  • Confirm the file is gone by running:
ls tests/

You should see an empty directory with no files listed.

  • Create a new file called login.spec.ts inside the tests folder and add the following code:
import { test, expect } from '@playwright/test';

// Group related login tests under one label
test.describe('Login Tests', () => {
  // Navigate to the login page before every test
  test.beforeEach(async ({ page }) => {
    await page.goto('https://www.saucedemo.com');
  });

  test('should log in with valid credentials', async ({ page }) => {
    // Fill in login credentials using placeholder text to find inputs
    await page.getByPlaceholder('Username').fill('standard_user');
    await page.getByPlaceholder('Password').fill('secret_sauce');
    // Click the Login button by its accessible role
    await page.getByRole('button', { name: 'Login' }).click();
    // Assert the browser navigated to the inventory page
    await expect(page).toHaveURL(/inventory\.html/);
  });
});

What does this code do?

  • test.describe groups related tests under one label. All login tests live inside this block.
  • test.beforeEach runs before every test in the group. Since both tests start at the login page, this avoids repeating the navigation call.
  • getByPlaceholder('Username') finds the input by its placeholder text, the same way a real user scans a form.
  • getByRole('button', { name: 'Login' }) finds the button by its accessible role. This is more resilient than targeting a CSS class that could change.
  • expect(page).toHaveURL(/inventory\.html/) uses a regex to assert the URL changed after login. Playwright automatically waits for the navigation to complete before checking.
  • Save login.spec.ts.

Why these locator strategies?

Playwright recommends a priority order for finding elements: getByRole first, then getByLabel, getByPlaceholder, getByText, and finally CSS selectors as a last resort.

Role-based and text-based locators mirror how real users interact with a page. They also survive UI redesigns better than fragile CSS class selectors.

Seeing a red underline in VS Code?

  • Make sure the file is saved inside the tests/ folder, not the project root.
  • Check that the import line reads @playwright/test exactly. A typo in the package name causes unresolved imports.

Still stuck? Help me fix an import error in my Playwright test file.

Add a test for a locked-out user

Swag Labs includes several test accounts with different behaviors. The locked_out_user account is blocked from logging in, which makes it perfect for testing error handling.

  • In login.spec.ts, find the closing }); of your first test (the one that ends after toHaveURL).
  • Add this second test right below it, still inside the test.describe block:
  test('should show error for locked out user', async ({ page }) => {
    // Attempt login with the locked-out account
    await page.getByPlaceholder('Username').fill('locked_out_user');
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
    // Target the error message using its data-test attribute
    const errorMessage = page.locator('[data-test="error"]');
    // Assert the error is visible and contains expected text
    await expect(errorMessage).toBeVisible();
    await expect(errorMessage).toContainText('locked out');
  });

What does this test do?

  • page.locator('[data-test="error"]') uses a CSS attribute selector to find the error container. Swag Labs marks key elements with data-test attributes, which are stable hooks designed for automation.
  • toBeVisible() is a web-first assertion. Playwright automatically retries the check until the element appears or the timeout expires. No manual waits needed.
  • toContainText('locked out') checks that the error message includes the phrase "locked out" without requiring an exact full-string match.
  • Save login.spec.ts.

Not sure where to add the second test?

The new test must sit inside the test.describe block, after the first test's closing }); but before the describe block's own closing });. Use the double-check tab below to compare your full file.

Help me place the second test correctly in my login.spec.ts file.

✔️ Awesome, I've got everything!

Great. Double check you've saved login.spec.ts before moving on.

ⓧ I'd like to double check the full code

Here is the complete login.spec.ts file. Make sure yours matches exactly.

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

test.describe('Login Tests', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://www.saucedemo.com');
  });

  test('should log in with valid credentials', async ({ page }) => {
    await page.getByPlaceholder('Username').fill('standard_user');
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page).toHaveURL(/inventory\.html/);
  });

  test('should show error for locked out user', async ({ page }) => {
    await page.getByPlaceholder('Username').fill('locked_out_user');
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
    const errorMessage = page.locator('[data-test="error"]');
    await expect(errorMessage).toBeVisible();
    await expect(errorMessage).toContainText('locked out');
  });
});
Run your tests and view the report

Playwright runs tests in headless mode by default, meaning you won't see a browser window. Headed mode launches a visible browser so you can watch each step happen in real time.

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

A Chromium browser window opens. You should see the Swag Labs login page load, the username and password fields fill in, and the page navigate to the inventory. For the second test, the login form fills in again and an error banner appears.

What should I see in the terminal?

After the browser closes, the terminal shows a summary like 2 passed. Both tests should pass with green checkmarks.

  • Open the HTML test report by running:
npx playwright show-report

A browser tab opens with the Playwright HTML report. You should see your two tests listed under the "Login Tests" group, both marked as passed.

Tests failing?

  • If you see a timeout error, check that https://www.saucedemo.com loads in your regular browser. The site may be temporarily down.
  • If the valid login test fails, double check that the username is standard_user and the password is secret_sauce. Both are case-sensitive.
  • If you see a "no tests found" error, make sure your file is named login.spec.ts (not login.test.ts) and lives inside the tests/ folder.

Help me debug my failing Playwright login tests.

Your first two tests are passing and you've seen the results in both the terminal and the HTML report. Next up, you'll extract these raw locators into reusable Page Object Model classes to keep your tests clean as the suite grows.

Build Page Object Model Classes

Your login tests catch both successful and failed logins. But look at the test code you wrote. Both tests repeat the same locators for the username field, password field, and login button.

If you needed to test login from five different spec files, you would copy those same locators five times. The Page Object Model pattern fixes this by giving each page its own class that holds all locators and actions in one place.

In this step, get ready to:
  • Create Page Object Model classes for the Login, Inventory, and Cart pages.
  • Refactor your login tests to use the LoginPage class.
  • Verify your refactored tests still pass.
Create the LoginPage class

Each Page Object Model class represents one page of your application. The class stores Playwright locators as properties and exposes methods for common user actions.

What is the Page Object Model?

Instead of writing page.getByPlaceholder('Username').fill('standard_user') in every test, you define a LoginPage class with a usernameInput locator and a login() method.

Your tests then call loginPage.login('standard_user', 'secret_sauce'). If the site changes a selector, you update one class instead of every test file.

  • Create the pages directory inside your tests folder by running this command:
mkdir tests/pages
  • Confirm the directory was created by running:
ls tests/

You should see pages/ listed alongside your login.spec.ts file.

  • Create a new file called LoginPage.ts inside the tests/pages/ folder.
  • Add the following code to tests/pages/LoginPage.ts:
import { type Locator, type Page } from '@playwright/test';

export class LoginPage {
  // Declare the page and locator properties as readonly
  readonly page: Page;
  readonly usernameInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    // Use getByPlaceholder and getByRole for resilient locators
    this.usernameInput = page.getByPlaceholder('Username');
    this.passwordInput = page.getByPlaceholder('Password');
    this.loginButton = page.getByRole('button', { name: 'Login' });
    // Use a data-test attribute selector for the error container
    this.errorMessage = page.locator('[data-test="error"]');
  }

  // Navigate to the login page
  async goto() {
    await this.page.goto('/');
  }

  // Fill in credentials and submit the form
  async login(username: string, password: string) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  // Return the text content of the error message
  async getErrorMessage(): Promise<string> {
    return await this.errorMessage.innerText();
  }
}

What does this code do?

  • The constructor takes a Playwright Page object and stores locators for every interactive element on the login page.
  • Each locator is readonly, so tests cannot accidentally overwrite them.
  • The login() method bundles the three actions (fill username, fill password, click Login) into a single call your tests can reuse.
  • The goto() method navigates to the site's root URL using a relative path. This works with Playwright's baseURL config, which you will set up in a later step.
  • Save tests/pages/LoginPage.ts.

You will not see a visible change yet. The next substep builds more page classes, and then you will refactor your tests to use them.

Create the InventoryPage and CartPage classes

The Swag Labs store has two more pages your tests will interact with: the product inventory page and the shopping cart page. Each one gets its own class.

  • Create a new file called InventoryPage.ts inside tests/pages/.
  • Add the following code to tests/pages/InventoryPage.ts:
import { expect, type Locator, type Page } from '@playwright/test';

export class InventoryPage {
  // Page and locator properties for the products page
  readonly page: Page;
  readonly pageTitle: Locator;
  readonly inventoryItems: Locator;
  readonly sortDropdown: Locator;
  readonly cartBadge: Locator;
  readonly cartLink: Locator;

  constructor(page: Page) {
    this.page = page;
    // All locators use data-test attributes for stability
    this.pageTitle = page.locator('[data-test="title"]');
    this.inventoryItems = page.locator('[data-test="inventory-item"]');
    this.sortDropdown = page.locator('[data-test="product-sort-container"]');
    this.cartBadge = page.locator('[data-test="shopping-cart-badge"]');
    this.cartLink = page.locator('[data-test="shopping-cart-link"]');
  }

  // Find a product by name and click its Add to cart button
  async addProductToCart(productName: string) {
    const product = this.inventoryItems.filter({ hasText: productName });
    await product.getByRole('button', { name: 'Add to cart' }).click();
  }

  // Return the number shown on the cart badge
  async getCartBadgeCount(): Promise<string> {
    return await this.cartBadge.innerText();
  }

  // Click the shopping cart icon to navigate to the cart page
  async openCart() {
    await this.cartLink.click();
  }
}

What does this code do?

  • The inventoryItems locator matches all product cards on the page. The filter({ hasText: productName }) call narrows it down to a specific product by name.
  • The cartBadge locator targets the small counter icon on the shopping cart. It only appears after you add at least one item.
  • All locators use data-test attribute selectors. These are stable test IDs that the Swag Labs site provides specifically for automation.
  • Save tests/pages/InventoryPage.ts.

You will not see a change yet. Next, create the CartPage class.

  • Create a new file called CartPage.ts inside tests/pages/.
  • Add the following code to tests/pages/CartPage.ts:
import { type Locator, type Page } from '@playwright/test';

export class CartPage {
  // Page and locator properties for the cart page
  readonly page: Page;
  readonly cartItems: Locator;
  readonly checkoutButton: Locator;

  constructor(page: Page) {
    this.page = page;
    // Cart items share the same data-test attribute as inventory items
    this.cartItems = page.locator('[data-test="inventory-item"]');
    this.checkoutButton = page.locator('[data-test="checkout"]');
  }

  // Return the number of items currently in the cart
  async getItemCount(): Promise<number> {
    return await this.cartItems.count();
  }

  // Collect the name of every item in the cart
  async getItemNames(): Promise<string[]> {
    const names: string[] = [];
    const items = await this.cartItems.all();
    for (const item of items) {
      const name = await item.locator('[data-test="inventory-item-name"]').innerText();
      names.push(name);
    }
    return names;
  }

  // Get the displayed price for a specific product
  async getItemPrice(productName: string): Promise<string> {
    const item = this.cartItems.filter({ hasText: productName });
    return await item.locator('[data-test="inventory-item-price"]').innerText();
  }

  // Click the Remove button on a specific product
  async removeItem(productName: string) {
    const item = this.cartItems.filter({ hasText: productName });
    await item.getByRole('button', { name: 'Remove' }).click();
  }

  // Click the Checkout button to proceed
  async proceedToCheckout() {
    await this.checkoutButton.click();
  }
}

What does this code do?

  • The getItemNames() method uses .all() to iterate over every cart item and collect each product's name into an array.
  • The removeItem() and getItemPrice() methods both use filter({ hasText }) to find a specific product by name before acting on it.
  • The getItemCount() method returns a number using .count(), which you will use in the next step to assert how many items remain after removals.
  • Save tests/pages/CartPage.ts.

You now have three page object classes ready. The next substep puts LoginPage to work by refactoring your existing tests.

Seeing red squiggly lines in VS Code?

  • Make sure each file is saved in the correct location. LoginPage.ts, InventoryPage.ts, and CartPage.ts must all be inside tests/pages/.
  • Check that the class names match the file names exactly. TypeScript is case-sensitive.
  • Why are my page object files showing TypeScript errors?
Refactor your login tests to use LoginPage

Your page object classes are ready. Now you can replace the raw locators in login.spec.ts with calls to the LoginPage class.

  • Open tests/login.spec.ts.
  • Replace the entire file contents with the following refactored code:
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

test.describe('Login Tests', () => {
  // Declare loginPage so all tests in this block can access it
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    // Create a fresh LoginPage instance and navigate before each test
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test('should log in with valid credentials', async ({ page }) => {
    // Use the LoginPage method instead of raw locators
    await loginPage.login('standard_user', 'secret_sauce');
    await expect(page).toHaveURL(/inventory\.html/);
  });

  test('should show error for locked out user', async () => {
    await loginPage.login('locked_out_user', 'secret_sauce');
    // Access the error locator through the LoginPage class
    await expect(loginPage.errorMessage).toBeVisible();
    await expect(loginPage.errorMessage).toContainText('locked out');
  });
});

What changed in the refactored code?

  • A new import { LoginPage } line at the top brings in the class you just created.
  • The let loginPage: LoginPage declaration lives outside beforeEach so every test in the describe block can access it.
  • The beforeEach hook now creates a new LoginPage instance and calls loginPage.goto() instead of page.goto('https://www.saucedemo.com').
  • Both tests now call loginPage.login() instead of repeating three lines of locator code. The second test accesses loginPage.errorMessage directly.
  • Save tests/login.spec.ts.
  • Run your refactored login tests by running this command:
npx playwright test tests/login.spec.ts

You should see both tests pass, just like before. The output shows 2 passed in the terminal.

Tests failing after the refactor?

  • Check that the import path matches your file structure. The import should be './pages/LoginPage' (no .ts extension).
  • Make sure LoginPage.ts is saved inside tests/pages/. If it is in the wrong folder, the import will fail with a module-not-found error.
  • If you see a navigation error from goto(), the relative '/' path requires a baseURL in your Playwright config. You will configure this in Step 5. For now, you can temporarily change the goto() method to use 'https://www.saucedemo.com' instead of '/'.
  • Why are my refactored login tests failing?

✔️ Awesome, I've got everything!

Great. Double check you have saved all four files before moving on.

ⓧ I'd like to double check the full code

Here is the full code for each file at this point. Compare yours against these.

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

export class LoginPage {
  readonly page: Page;
  readonly usernameInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.usernameInput = page.getByPlaceholder('Username');
    this.passwordInput = page.getByPlaceholder('Password');
    this.loginButton = page.getByRole('button', { name: 'Login' });
    this.errorMessage = page.locator('[data-test="error"]');
  }

  async goto() {
    await this.page.goto('/');
  }

  async login(username: string, password: string) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async getErrorMessage(): Promise<string> {
    return await this.errorMessage.innerText();
  }
}
import { expect, type Locator, type Page } from '@playwright/test';

export class InventoryPage {
  readonly page: Page;
  readonly pageTitle: Locator;
  readonly inventoryItems: Locator;
  readonly sortDropdown: Locator;
  readonly cartBadge: Locator;
  readonly cartLink: Locator;

  constructor(page: Page) {
    this.page = page;
    this.pageTitle = page.locator('[data-test="title"]');
    this.inventoryItems = page.locator('[data-test="inventory-item"]');
    this.sortDropdown = page.locator('[data-test="product-sort-container"]');
    this.cartBadge = page.locator('[data-test="shopping-cart-badge"]');
    this.cartLink = page.locator('[data-test="shopping-cart-link"]');
  }

  async addProductToCart(productName: string) {
    const product = this.inventoryItems.filter({ hasText: productName });
    await product.getByRole('button', { name: 'Add to cart' }).click();
  }

  async getCartBadgeCount(): Promise<string> {
    return await this.cartBadge.innerText();
  }

  async openCart() {
    await this.cartLink.click();
  }
}
import { type Locator, type Page } from '@playwright/test';

export class CartPage {
  readonly page: Page;
  readonly cartItems: Locator;
  readonly checkoutButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.cartItems = page.locator('[data-test="inventory-item"]');
    this.checkoutButton = page.locator('[data-test="checkout"]');
  }

  async getItemCount(): Promise<number> {
    return await this.cartItems.count();
  }

  async getItemNames(): Promise<string[]> {
    const names: string[] = [];
    const items = await this.cartItems.all();
    for (const item of items) {
      const name = await item.locator('[data-test="inventory-item-name"]').innerText();
      names.push(name);
    }
    return names;
  }

  async getItemPrice(productName: string): Promise<string> {
    const item = this.cartItems.filter({ hasText: productName });
    return await item.locator('[data-test="inventory-item-price"]').innerText();
  }

  async removeItem(productName: string) {
    const item = this.cartItems.filter({ hasText: productName });
    await item.getByRole('button', { name: 'Remove' }).click();
  }

  async proceedToCheckout() {
    await this.checkoutButton.click();
  }
}
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

test.describe('Login Tests', () => {
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test('should log in with valid credentials', async ({ page }) => {
    await loginPage.login('standard_user', 'secret_sauce');
    await expect(page).toHaveURL(/inventory\.html/);
  });

  test('should show error for locked out user', async () => {
    await loginPage.login('locked_out_user', 'secret_sauce');
    await expect(loginPage.errorMessage).toBeVisible();
    await expect(loginPage.errorMessage).toContainText('locked out');
  });
});

Your page objects are ready and your login tests are cleaner than ever. Next up, you will put InventoryPage and CartPage to work by testing the full shopping cart flow.

Test the Shopping Cart Flow

Your Page Object classes are built and your login tests prove the pattern works. Now it's time to put those page objects to real use by testing the core shopping experience.

In this step, you'll create a new test file that exercises the cart flow on Swag Labs. Your tests will add products, verify prices, and remove items to confirm the store behaves correctly.

In this step, get ready to:
  • Write a test that adds a product to the cart and verifies the cart badge updates.
  • Write a test that adds multiple products, checks prices, and removes an item.
  • Run the full test suite and verify all tests pass.
Create the cart test file

Every cart test needs a logged-in user on the inventory page before it runs. Playwright's test.beforeEach hook handles this automatically, the same pattern you used in login.spec.ts.

  • Create a new file called cart.spec.ts inside your tests/ folder.
  • Add the following code to set up the test suite with a login hook and your first cart test:
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { InventoryPage } from './pages/InventoryPage';
import { CartPage } from './pages/CartPage';

test.describe('Shopping Cart Tests', () => {
  let loginPage: LoginPage;
  let inventoryPage: InventoryPage;
  let cartPage: CartPage;

  // Log in before every test so each one starts on the inventory page
  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    inventoryPage = new InventoryPage(page);
    cartPage = new CartPage(page);
    await loginPage.goto();
    await loginPage.login('standard_user', 'secret_sauce');
    await expect(page).toHaveURL(/inventory\.html/);
  });

  // Verify adding a single product updates the cart badge
  test('should add a product to the cart', async () => {
    await inventoryPage.addProductToCart('Sauce Labs Backpack');
    const badgeCount = await inventoryPage.getCartBadgeCount();
    expect(badgeCount).toBe('1');
  });
});

What does this code do?

  • The imports bring in all three Page Object classes. Even though this first test only uses InventoryPage, the next test will need CartPage too.
  • test.beforeEach creates fresh page object instances and logs in as standard_user before every test. The URL assertion confirms the login succeeded before any cart actions begin.
  • The first test calls addProductToCart() from InventoryPage, then reads the cart badge text and checks it equals '1'.
  • Save cart.spec.ts.
  • Run the cart test in headed mode to watch it execute by running:
npx playwright test tests/cart.spec.ts --headed

You should see a browser window open, log in to Swag Labs, add the Backpack to the cart, and close. The terminal should show 1 test passed.

Test failing or browser not opening?

  • If you see an import error, make sure your file is saved inside the tests/ folder (not the project root).
  • If the login assertion fails, confirm your LoginPage's goto() method navigates to '/'. Without a baseURL configured yet, Playwright may not resolve relative URLs. You'll fix this in Step 5.
  • help me debug my cart test
Write the add-and-remove test

The first test verified a single add-to-cart action. A realistic shopping flow involves multiple products, price verification, and removing items. This next test covers all of those scenarios in one sequence.

Why one long test instead of many small ones?

End-to-end tests are expensive to run because each one launches a browser and logs in. Grouping related assertions into one test reduces that overhead while still covering the full user journey.

The CartPage class you built in the previous step gives you the methods you need here: getItemNames(), getItemPrice(), removeItem(), and getItemCount().

  • In tests/cart.spec.ts, find the closing }); of the first test ('should add a product to the cart').
  • Add this second test below it, still inside the test.describe block:
  test('should add and remove products from the cart', async () => {
    // Add two products and verify the cart badge updates
    await inventoryPage.addProductToCart('Sauce Labs Backpack');
    await inventoryPage.addProductToCart('Sauce Labs Bike Light');
    const badgeCount = await inventoryPage.getCartBadgeCount();
    expect(badgeCount).toBe('2');

    // Navigate to the cart page
    await inventoryPage.openCart();

    // Verify both items appear in the cart
    const itemNames = await cartPage.getItemNames();
    expect(itemNames).toContain('Sauce Labs Backpack');
    expect(itemNames).toContain('Sauce Labs Bike Light');

    // Check that prices match the expected values
    const backpackPrice = await cartPage.getItemPrice('Sauce Labs Backpack');
    expect(backpackPrice).toBe('$29.99');

    const bikeLightPrice = await cartPage.getItemPrice('Sauce Labs Bike Light');
    expect(bikeLightPrice).toBe('$9.99');

    // Remove the Bike Light and verify only the Backpack remains
    await cartPage.removeItem('Sauce Labs Bike Light');

    const updatedCount = await cartPage.getItemCount();
    expect(updatedCount).toBe(1);

    const updatedNames = await cartPage.getItemNames();
    expect(updatedNames).toContain('Sauce Labs Backpack');
    expect(updatedNames).not.toContain('Sauce Labs Bike Light');
  });

What does this test do?

  • It adds two products (Sauce Labs Backpack at $29.99 and Sauce Labs Bike Light at $9.99) and checks the badge shows '2'.
  • openCart() navigates to the cart page, where getItemNames() and getItemPrice() verify both items are present with the correct prices.
  • After removing the Bike Light, the test confirms the item count drops to 1 and the Backpack is the only remaining product. This verifies that the remove action updates the cart state correctly.
  • Save cart.spec.ts.

Seeing a price assertion error?

  • Make sure the product names in your test match exactly: 'Sauce Labs Backpack' and 'Sauce Labs Bike Light'. The filter({ hasText }) in your Page Object matches on these strings.
  • If the price assertion fails, check that you included the dollar sign in the expected value ('$29.99' not '29.99').
  • help me debug my cart price assertion

✔️ Awesome, I've got everything!

Double check you've saved cart.spec.ts.

ⓧ I'd like to double check the full code

Here is the complete cart.spec.ts file. Compare your code against this:

import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { InventoryPage } from './pages/InventoryPage';
import { CartPage } from './pages/CartPage';

test.describe('Shopping Cart Tests', () => {
  let loginPage: LoginPage;
  let inventoryPage: InventoryPage;
  let cartPage: CartPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    inventoryPage = new InventoryPage(page);
    cartPage = new CartPage(page);
    await loginPage.goto();
    await loginPage.login('standard_user', 'secret_sauce');
    await expect(page).toHaveURL(/inventory\.html/);
  });

  test('should add a product to the cart', async () => {
    await inventoryPage.addProductToCart('Sauce Labs Backpack');
    const badgeCount = await inventoryPage.getCartBadgeCount();
    expect(badgeCount).toBe('1');
  });

  test('should add and remove products from the cart', async () => {
    await inventoryPage.addProductToCart('Sauce Labs Backpack');
    await inventoryPage.addProductToCart('Sauce Labs Bike Light');
    const badgeCount = await inventoryPage.getCartBadgeCount();
    expect(badgeCount).toBe('2');

    await inventoryPage.openCart();

    const itemNames = await cartPage.getItemNames();
    expect(itemNames).toContain('Sauce Labs Backpack');
    expect(itemNames).toContain('Sauce Labs Bike Light');

    const backpackPrice = await cartPage.getItemPrice('Sauce Labs Backpack');
    expect(backpackPrice).toBe('$29.99');

    const bikeLightPrice = await cartPage.getItemPrice('Sauce Labs Bike Light');
    expect(bikeLightPrice).toBe('$9.99');

    await cartPage.removeItem('Sauce Labs Bike Light');

    const updatedCount = await cartPage.getItemCount();
    expect(updatedCount).toBe(1);

    const updatedNames = await cartPage.getItemNames();
    expect(updatedNames).toContain('Sauce Labs Backpack');
    expect(updatedNames).not.toContain('Sauce Labs Bike Light');
  });
});
Run the full test suite

You now have four tests across two spec files: two login tests and two cart tests. Time to run them all together and confirm nothing is broken.

  • Run the entire test suite by running:
npx playwright test

You should see all 4 tests pass in the terminal output. The results show each test grouped by spec file.

  • Open the HTML report to view your results by running:
npx playwright show-report

The report opens in your browser. You should see both spec files listed with all tests showing green checkmarks.

Some tests failing?

  • If only the cart tests fail, check that your test.beforeEach in cart.spec.ts includes await loginPage.goto() before calling loginPage.login(). Without it, Playwright has no page to interact with.
  • If you see a timeout error, the Swag Labs site may be temporarily slow. Playwright's auto-waiting handles most delays, but network issues can exceed the default timeout.
  • If login tests pass but cart tests fail, compare your cart.spec.ts against the full code in the double-check tab above.
  • help me debug my failing Playwright tests

Your cart tests prove that the Page Object Model pays off. Three page objects work together to test a full shopping flow, and the test code reads like a user story. Next up, you'll configure cross-browser testing and improve your reporting.

Configure Cross-Browser Testing and Reporting

Your test suite now covers login validation, product browsing, and cart operations. That is a solid foundation of test coverage.

Real-world test suites run across multiple browsers to catch rendering differences and browser-specific bugs. In this step, you will configure Playwright to test on both Chromium and Firefox, and add debugging tools like automatic screenshots and failure traces.

In this step, get ready to:
  • Update playwright.config.ts with a base URL, retry settings, and failure screenshots.
  • Add Firefox as a second browser target.
  • Verify your tests run across both browsers and view the HTML report.
Update the Playwright configuration

Playwright's configuration file controls how your tests behave across environments. Right now, your config has no base URL, no local retries, and no automatic screenshots on failure.

  • In VS Code, open playwright.config.ts.
  • Find the retries line:
retries: process.env.CI ? 2 : 0,
  • Change the 0 at the end to 1, so the line looks like this:
retries: process.env.CI ? 2 : 1,

What does this change?

This tells Playwright to retry a failed test once when running locally. In CI, it retries twice. Retries help catch flaky tests that fail due to timing issues rather than real bugs.

Next, you will add a base URL and screenshot settings to the use block.

  • Find the use block in playwright.config.ts. It currently looks like this:
  use: {
    trace: 'on-first-retry',
  },
  • Replace the entire use block with this updated version:
  use: {
    // Base URL for all page.goto() calls with relative paths
    baseURL: 'https://www.saucedemo.com',
    // Record a trace on the first retry to help debug failures
    trace: 'on-first-retry',
    // Capture a screenshot when a test fails
    screenshot: 'only-on-failure',
  },

What do these settings do?

  • baseURL sets a default domain for all relative navigation calls. When your code calls page.goto('/'), Playwright resolves it to https://www.saucedemo.com/.
  • trace: 'on-first-retry' records a detailed trace file when Playwright retries a failed test. You can replay the trace to see every action, network request, and DOM snapshot.
  • screenshot: 'only-on-failure' captures a screenshot of the browser at the moment a test fails. This is invaluable for debugging failures in CI where you cannot watch the browser.
  • Save playwright.config.ts.

Seeing a syntax error?

Make sure every property inside the use block ends with a comma, including the last one. Also check that the opening and closing braces match.

Help me fix a syntax error in my playwright.config.ts use block.

Add Firefox to the projects array

Your config currently runs tests in Chromium only. Adding Firefox catches browser-specific issues that Chromium alone would miss.

  • In playwright.config.ts, scroll down to the projects array. It currently has a single Chromium entry:
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
  • Add a Firefox project after the Chromium entry, so the full projects array looks like this:
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    // Run the same tests in Firefox to catch cross-browser issues
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
  ],

What does this do?

Each entry in the projects array defines a browser configuration. Playwright runs your entire test suite once per project. With two projects, you get test results for both Chromium and Firefox in a single npx playwright test run.

  • Save playwright.config.ts.
  • Run the full test suite by running this command:
npx playwright test

You should see output showing tests running in both Chromium and Firefox. The total test count doubles because every test runs in each browser.

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

The report opens in your browser. You should see your tests listed with results for both Chromium and Firefox. You can filter by browser using the dropdown at the top of the report.

Tests failing in Firefox?

Make sure Firefox browsers are installed. Run npx playwright install to download any missing browser binaries. If only Firefox tests fail, check the HTML report for the specific error message.

Help me debug Firefox test failures in Playwright.

✔️ Awesome, I've got everything!

Double check you have saved playwright.config.ts.

ⓧ I'd like to double check the full code

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 1,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'https://www.saucedemo.com',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
  ],
});
Verify relative paths work with the base URL

Now that baseURL is configured, every page.goto('/') call in your tests resolves to https://www.saucedemo.com/. Your LoginPage class already uses this relative path in its goto() method.

  • Open tests/pages/LoginPage.ts and confirm the goto() method uses a relative path:
  async goto() {
    await this.page.goto('/');
  }

Why use relative paths?

Relative paths like '/' keep your tests flexible. If the base URL changes (for example, when testing against a staging environment), you only update one line in playwright.config.ts instead of editing every test file.

  • Run the full test suite one more time to confirm everything passes with the base URL by running this command:
npx playwright test

All tests should pass across both Chromium and Firefox. The terminal output shows a summary with a count per browser.

Tests failing after adding the base URL?

Check that baseURL in your config is exactly 'https://www.saucedemo.com' with no trailing slash. Also make sure your LoginPage.goto() calls page.goto('/') with the leading slash.

Help me debug baseURL issues in my Playwright config.

Your test suite now runs across two browsers with automatic failure screenshots and retry logic. Next up, you will push everything to GitHub and set up a CI/CD pipeline that runs your tests automatically on every push.

Set Up CI/CD with GitHub Actions

Your test suite runs across two browsers and generates a polished HTML report. All of that works on your machine, but a test suite only proves its value when it runs automatically in a clean environment on every code change.

In this step, you'll set up a CI/CD pipeline using GitHub Actions. Every push to your repository will trigger the full Playwright suite, and the HTML report gets uploaded as a downloadable artifact.

In this step, get ready to:
  • Initialize a Git repository and create a .gitignore file.
  • Push your project to GitHub and trigger the CI/CD pipeline.
  • Download and view the HTML test report from the pipeline run.
Initialize your Git repository

Git tracks every change you make to your project files. Before you can push code to GitHub, you need to initialize a local Git repository.

  • Initialize a Git repository in your project folder by running this command:
git init

You should see a message like Initialized empty Git repository in ... confirming it worked.

Your project contains files that should never be committed to Git. Dependencies, test output, and report folders are generated files that clutter a repository and slow down collaborators.

  • In the VS Code Explorer sidebar, click the New File icon at the top of your project folder.
  • Name the file .gitignore (including the leading dot).
  • Paste the following content into .gitignore:
node_modules/
test-results/
playwright-report/
blob-report/

What does each line exclude?

  • node_modules/ contains all installed packages. Collaborators regenerate this with npm ci.
  • test-results/ holds raw output from test runs (traces, screenshots on failure).
  • playwright-report/ and blob-report/ store the generated HTML reports. These are rebuilt on every test run.
  • Save .gitignore.
  • Confirm the file exists by running:
ls -a

You should see .gitignore and .git listed in the output alongside your project files.

Don't see .gitignore in the list?

Make sure you included the leading dot in the filename. Files starting with . are hidden by default, which is why you need the -a flag in ls to see them.

Help me create a .gitignore file in my Playwright project

Now take a look at the CI pipeline that Playwright already generated for you during the initial setup.

  • In VS Code, expand the .github folder, then the workflows folder inside it.
  • Open playwright.yml. Here is what the file looks like:
name: Playwright Tests
on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v5
    - uses: actions/setup-node@v5
      with:
        node-version: lts/*
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      run: npx playwright test
    - uses: actions/upload-artifact@v4
      if: ${{ !cancelled() }}
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

What does this workflow do?

This file tells GitHub Actions to run your tests automatically whenever code is pushed to main or master, or when a pull request targets those branches.

  • actions/checkout@v5 clones your repository into the runner.
  • actions/setup-node@v5 installs the latest LTS version of Node.js.
  • npm ci installs your project dependencies from the lockfile.
  • npx playwright install --with-deps downloads browser binaries (Chromium, Firefox) on the CI runner.
  • npx playwright test runs your entire test suite.
  • actions/upload-artifact@v4 saves the HTML report as a downloadable artifact with 30-day retention.

Why npm ci instead of npm install?

In CI environments, npm ci is preferred because it installs exact versions from package-lock.json. This guarantees your CI runner uses the same dependency versions as your local machine. npm install can resolve to newer patch versions, which could introduce subtle differences.

✔️ Awesome, I've got everything!

Double check you've saved .gitignore. The workflow file doesn't need changes.

ⓧ I'd like to double check the full code

Here is the complete .gitignore file:

node_modules/
test-results/
playwright-report/
blob-report/

And here is the complete .github/workflows/playwright.yml file:

name: Playwright Tests
on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v5
    - uses: actions/setup-node@v5
      with:
        node-version: lts/*
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      run: npx playwright test
    - uses: actions/upload-artifact@v4
      if: ${{ !cancelled() }}
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30
Push to GitHub and trigger the pipeline

With your repository initialized and the workflow file in place, you need a remote repository on GitHub to push to. Once your code lands there, GitHub Actions picks up the workflow file and runs it automatically.

  • Stage and commit all your project files by running these commands:
git add .
git commit -m "Initial commit: Playwright test suite"

You should see a list of files being committed. The files in your .gitignore (like node_modules/) will not appear in that list.

Now create a remote repository on GitHub to push your code to.

  • Open github.com in your browser and sign in.
  • Click the + icon in the top-right corner of the page.
  • Select New repository.
  • Enter playwright-ecommerce-tests as the Repository name.
  • Leave the repository set to Public.
  • Do NOT check any of the initialization options (README, .gitignore, or license). You already have a local repository ready to push.
  • Click Create repository.

GitHub shows a quick setup page with commands for pushing an existing repository. You'll use those commands next.

  • Back in your terminal, add the remote origin and push your code by running these commands:
git branch -M main
git remote add origin [[YOUR_REPO_URL="https://github.com/your-username/playwright-ecommerce-tests.git"]]
git push -u origin main

What do these commands do?

  • git branch -M main renames your default branch to main if it isn't already.
  • git remote add origin links your local repository to the GitHub repository you just created.
  • git push -u origin main uploads all your committed files to GitHub and sets main as the default upstream branch.

GitHub may ask you to authenticate

If this is your first time pushing to GitHub from the terminal, you may see a browser pop-up or a credential prompt. Follow the prompts to sign in with your GitHub account. If you're asked to choose an authentication method, a personal access token or SSH key both work.

  • Go back to your repository page on GitHub and refresh the page.
  • You should see all your project files listed: playwright.config.ts, the tests/ folder, .github/workflows/, and .gitignore.
  • Click the Actions tab at the top of your repository page.

You should see a workflow run called Initial commit: Playwright test suite in progress or already completed. The yellow circle means it's running. A green checkmark means all tests passed.

  • Click the workflow run name to see the details.
  • Click the test job to expand the step-by-step logs.
  • Expand the Run Playwright tests step to see your test results in the terminal output.

You should see your login and cart tests running across both Chromium and Firefox, with all tests passing.

Seeing a red X on the workflow?

Click into the failed job and expand the failing step to read the error message. Common causes include a test that passes locally but fails in CI due to timing differences. Check that your playwright.config.ts has retries: process.env.CI ? 2 : 1 to give flaky tests a second chance in CI.

Also verify that your .gitignore did not accidentally exclude the tests/ folder or playwright.config.ts. Those files must be committed for CI to find them.

Help me debug my failing GitHub Actions Playwright workflow

Download and view the CI test report

The workflow uploads your HTML report as an artifact after every run. You can download this report from GitHub and open it locally to get the same interactive view you've been using throughout the project.

  • On your workflow run page in GitHub, scroll down to the Artifacts section at the bottom.
  • Click playwright-report to download the artifact as a ZIP file.
  • Extract the ZIP file to a folder on your computer.
  • Open the report by running this command, replacing the path with the location of your extracted folder:
npx playwright show-report [[REPORT_PATH="path/to/extracted/playwright-report"]]

What does this command do?

The show-report command spins up a local web server and opens the HTML report in your browser. When you pass a path argument, it serves the report from that specific folder instead of the default playwright-report/ directory.

Your browser opens with the full HTML report from the CI run. You should see all your login and cart tests listed with green checkmarks across both Chromium and Firefox.

  • Click on individual test names to see their execution details.
  • Use the browser filter at the top to toggle between Chromium and Firefox results.

Every test passing in CI confirms your suite is portable. It runs the same way on a fresh Ubuntu machine as it does on your local environment.

Report not opening in the browser?

Make sure you extracted the ZIP file before running the command. The path should point to the folder containing an index.html file, not the ZIP file itself.

If you see a "No report found" error, double check the folder path. Try running ls on the extracted folder to confirm index.html is inside.

Help me open my downloaded Playwright report

Your test suite now runs automatically on every push, with a downloadable report for anyone on your team to review. Next up, stretch your skills with a Secret Mission that tests some tricky edge cases.

Secret mission

Test the Problem Users

The Swag Labs store has several user accounts that behave unexpectedly. Some are locked out, some see broken images, and some have cart functionality that silently fails. Your mission is to write tests that catch these defects automatically.

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 (plus a free GitHub repository), so there are no ongoing costs.

Resources you used:

  • Playwright project directory on your Desktop (source code, Page Object classes, test specs).
  • Playwright browser binaries (Chromium, Firefox) downloaded to your local cache.
  • Node.js packages installed in your project's node_modules/ folder.
  • GitHub repository with your test suite and GitHub Actions workflow.
  • Playwright Test for VS Code extension.

Keep everything running

No action needed. Choose this if you are still actively building on your test suite or plan to extend it soon.

  • Your project folder, browser binaries, and GitHub repository all stay as they are.
  • GitHub Actions will continue running your tests on every push to main or master. This is free for public repositories.

Pause - I'll come back to this later

Free up local disk space from the browser binaries while keeping your project files and GitHub repository intact.

  • Remove the cached Playwright browsers by running this command in your project folder:
npx playwright uninstall --all
  • Your source code, test specs, and GitHub repository remain untouched.
  • When you are ready to resume, run npx playwright install to re-download the browsers.

Delete - I don't want to use this again

Remove all project resources from your machine and GitHub.

  • Remove the cached Playwright browsers by running:
npx playwright uninstall --all
  • Delete your project folder from the Desktop.
  • To delete the GitHub repository, go to your repository page on GitHub.
  • Click Settings.
  • Scroll to the bottom of the page and click Delete this repository under the Danger Zone section.
  • Follow the confirmation prompts to permanently delete it.
  • To uninstall the Playwright Test for VS Code extension, open VS Code and click the Extensions icon in the left sidebar.
  • Search for Playwright Test, click on it, and select Uninstall.

Nice Work!

Nice Work!

You just built a professional end-to-end test suite from scratch. Your Playwright project now tests a real e-commerce site across multiple browsers, uses a clean architecture pattern, and runs automatically in the cloud on every push.

You've learned how to:

  • Write end-to-end tests covering login, product browsing, and shopping cart flows using Playwright's built-in locators and web-first assertions.
  • Implement the Page Object Model pattern to encapsulate page interactions and keep your tests maintainable.
  • Set up a GitHub Actions CI/CD pipeline that runs your test suite automatically on every push and publishes HTML reports as downloadable artifacts.
  • Secret Mission: Tested edge cases with problem user accounts, practicing negative testing and visual defect detection.

Ready to quiz yourself?