Cross-Browser Tests with Playwright

Automate e-commerce tests across 3 browsers and 2 mobile viewports.

Introduction

30 Second Summary

Ever opened a website on your phone only to find buttons overlapping, text cut off, or features completely broken? That's what happens when web apps aren't tested across different browsers and screen sizes.

In this project, you will build an automated test suite using Playwright that verifies an e-commerce website works correctly across Chrome, Firefox, Safari, and mobile device viewports like iPhone and Android.

What You'll Build

A cross-browser testing suite written in TypeScript that automates product search, filtering, and cart operations on a real e-commerce site across 3 desktop browsers and 2 mobile viewports.

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

  • A working Playwright test suite that tests an e-commerce shopping flow across Chromium, Firefox, and WebKit (Safari's engine).
  • Mobile device emulation tests running on iPhone 14 and Pixel 7 viewports that catch responsive layout differences.
  • Generated HTML test reports and trace files you can use to debug failures visually.
  • Secret Mission: Add visual regression testing by capturing and comparing screenshots across browsers.

Are there any prerequisites?

You'll need Node.js 20.x or higher installed, along with VS Code. A basic understanding of web applications (HTML, CSS, JavaScript concepts) is helpful but not required.

Set Up Playwright with TypeScript

Playwright is a test automation framework that runs your tests across Chromium, Firefox, and WebKit with a single command. It handles the complexity of different browser engines so you can focus on writing tests.

In this step, you will create a new project folder, install Playwright with TypeScript, and verify everything works by running the built-in example tests across all three browsers.

In this step, get ready to:
  • Install Playwright with TypeScript in a new project folder.
  • Run the default example tests across three browser engines.
  • View the HTML test report dashboard.
Create your project folder and install Playwright

You need a dedicated folder for your test suite. You will create one on your Desktop so it is easy to find later.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal 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
  • Confirm the folder was created by running:
ls

You should see the playwright-ecommerce folder listed in your Desktop directory.

Now you will initialize a Playwright project inside this folder. The setup command launches an interactive wizard that asks you a few configuration questions.

  • Initialize the Playwright project by running this command:
npm init playwright@latest

What will the wizard ask?

The wizard prompts you with four questions. Answer them as follows:

  • TypeScript or JavaScript: select TypeScript (the default).
  • Tests folder name: press Enter to accept the default tests folder.
  • Add a GitHub Actions workflow: enter false or press N to skip this.
  • Install Playwright browsers: press Enter to accept true (the default). This downloads Chromium, Firefox, and WebKit binaries.

The browser download may take a minute or two depending on your internet speed. Wait for the command to finish completely before continuing.

  • Verify 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 did Playwright just create?

  • playwright.config.ts centralizes your test configuration: which browsers to run, timeouts, retries, and reporters.
  • tests/example.spec.ts is a minimal starter test that visits the Playwright website and checks basic page elements.
  • package.json tracks the @playwright/test dependency for your project.

Command not found or install failed?

  • If you see npm: command not found, Node.js is not installed or not in your PATH. Verify with node --version first.
  • If the browser download stalls, check your internet connection and try running npx playwright install separately after the init completes.
  • help me troubleshoot my Playwright installation
Run the example tests across all browsers

The scaffold includes a starter test file that visits the Playwright documentation site. Running it confirms that all three browser engines are installed and working correctly on your machine.

  • Run the full test suite by executing this command:
npx playwright test

You should see output showing 6 passed tests. Playwright runs the 2 example tests against each of the 3 configured browsers (Chromium, Firefox, WebKit).

What does the output mean?

Each line in the terminal output shows the browser name in square brackets, the test file, and the test title. A green checkmark means the test passed. The tests run headless by default, meaning no browser window opens on your screen.

The format looks like: ✓ [chromium] › example.spec.ts › has title.

Tests failed or timed out?

  • The default example tests connect to playwright.dev over the internet. Make sure you have an active internet connection.
  • If you see browser launch errors on Windows, ensure you are running Windows 11 or later. Playwright 1.59.1 requires Windows 11+ or Windows Server 2019+.
  • help me debug why my Playwright tests failed
Open the HTML test report

Playwright generates a detailed HTML report after every test run. This report lets you filter results by browser, see pass/fail status, and drill into individual test steps.

  • Open the HTML report in your browser by running this command:
npx playwright show-report

A browser tab opens showing the Playwright HTML Reporter. You should see all 6 tests listed with green checkmarks, grouped by browser project.

What can I do in this report?

The HTML Reporter is a dashboard you will use throughout this project. You can filter results by browser name, click into a failed test to see screenshots and error messages, and review the step-by-step execution timeline.

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

✔️ Awesome, I've got everything!

Great. Double check you still have your terminal open in the playwright-ecommerce folder for the next step.

ⓧ I'd like to double check the full code

Here is the default playwright.config.ts that Playwright generated. Yours should look like this:

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

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

Minor differences are okay

Your generated config may include extra comments or slightly different formatting. The important parts are the three browser projects (chromium, firefox, webkit), the html reporter, and trace: 'on-first-retry' in the use block.

Your Playwright environment is ready. Next up, you will write your first real test that navigates to an e-commerce site and verifies products load correctly.

Write Your First Test - Product Search and Filter

Your Playwright environment is working and your example tests pass across all three browsers. Now it's time to write tests that target a real web application.

You'll test an e-commerce shopping page at practiceautomatedtesting.com. This site has product search, filtering, and cart operations. In this step, you'll write tests that verify the product search and stock filter features work correctly.

In this step, get ready to:
  • Configure a base URL so your tests can use relative paths.
  • Create a test file that verifies 10 products load on the shopping page.
  • Add tests for search filtering and stock availability filtering.
Configure the base URL and create the test file

Playwright's baseURL setting lets you use relative paths like /shopping in your tests instead of typing the full URL every time. This keeps your tests cleaner and makes it easy to point your entire suite at a different environment later.

  • In VS Code, open playwright.config.ts from the file explorer sidebar.
  • Find the use section, which currently looks like this:
use: {
  trace: 'on-first-retry',
},
  • Add the baseURL line above trace, so the section now looks like this:
use: {
  baseURL: 'https://practiceautomatedtesting.com',
  trace: 'on-first-retry',
},
  • Save playwright.config.ts.

Now create the test file. You'll use Playwright locators to find elements on the page and assertions to verify they behave correctly.

  • Create a new file called shopping.spec.ts inside the tests folder.
  • Add the following code to set up the test describe block and your first test:
import { test, expect } from '@playwright/test';

test.describe('Product Search and Filter', () => {
  // Navigate to the shopping page before every test
  test.beforeEach(async ({ page }) => {
    await page.goto('/shopping');
  });

  // Verify the page loads all 10 products
  test('should display 10 products on initial load', async ({ page }) => {
    const productCards = page.locator('[data-testid="product-card"]');
    await expect(productCards).toHaveCount(10);
  });
});

What does this code do?

test.describe groups related tests together under a shared name. This makes your HTML report easier to read.

test.beforeEach runs before every test in the group. Here it navigates to /shopping, which Playwright combines with your baseURL to form the full URL.

page.locator('[data-testid="product-card"]') finds all elements with that test ID attribute. toHaveCount(10) asserts that exactly 10 product cards are on the page.

What are locators?

Locators are how Playwright finds elements on a page. The data-testid attribute is the most stable selector because it's purpose-built for testing and won't change when developers restyle the page.

Playwright also offers semantic locators like getByRole(), getByLabel(), and getByPlaceholder() for targeting elements by their accessible properties. You'll use a mix of these throughout this project.

  • Save tests/shopping.spec.ts.
  • Run your new test across all browsers by running this command:
npx playwright test tests/shopping.spec.ts
  • You should see output showing 3 passed tests (1 test across 3 browsers: Chromium, Firefox, WebKit).

Tests failing or timing out?

If you see a timeout error, make sure you have an internet connection. The test navigates to a live website.

If the test fails with "expected 10 received 0", the data-testid selector might not match. Open https://practiceautomatedtesting.com/shopping in your browser, right-click a product card, and select **Inspect** to check the actual attribute name.

help me debug why my Playwright toHaveCount assertion is failing on the shopping page

Test the search functionality

The shopping page has a search input that filters products in real time. Your next test will type a search term and verify only the matching product remains visible.

  • In tests/shopping.spec.ts, add the following test inside the test.describe block, after the first test's closing });:
  // Search for a specific product and verify filtered results
  test('should filter products by search term', async ({ page }) => {
    const searchInput = page.getByPlaceholder('Search products...');
    await searchInput.fill('Keyboard');

    const productCards = page.locator('[data-testid="product-card"]');
    await expect(productCards).toHaveCount(1);
    await expect(page.getByText('Mechanical Keyboard')).toBeVisible();
  });

What does this code do?

getByPlaceholder('Search products...') finds the search input by its placeholder text. This is a semantic locator that makes your test readable without needing to know internal CSS selectors.

fill('Keyboard') types the search term into the input. Playwright's auto-waiting then gives the page time to filter before the assertions run.

The two assertions verify that only 1 product card remains and that it's specifically the Mechanical Keyboard.

  • Save tests/shopping.spec.ts.
  • Run the tests again:
npx playwright test tests/shopping.spec.ts
  • You should now see 6 passed tests (2 tests across 3 browsers).

Search test failing?

If the placeholder locator fails, the actual placeholder text on the site might differ slightly. Open the shopping page in your browser, right-click the search input, select **Inspect**, and check the placeholder attribute value. Update your test to match exactly.

help me fix my Playwright getByPlaceholder locator for the search input

Test the stock availability filter

The shopping page also has an "In Stock Only" toggle that hides out-of-stock products. The HD Monitor is out of stock, so toggling this filter should make it disappear from the list.

  • Add this test after the search test, still inside the test.describe block:
  // Toggle stock filter and verify out-of-stock items disappear
  test('should hide out-of-stock items when filter is toggled', async ({ page }) => {
    const inStockToggle = page.getByLabel('In Stock Only');
    await inStockToggle.check();

    await expect(page.getByText('HD Monitor')).not.toBeVisible();
  });

What does this code do?

getByLabel('In Stock Only') finds the toggle by its associated label. This mimics how a real user would identify the control on the page.

check() activates the toggle. The not.toBeVisible() assertion then confirms the HD Monitor (which is out of stock) has been hidden from the product list.

  • Save tests/shopping.spec.ts.
  • Run all three tests:
npx playwright test tests/shopping.spec.ts
  • You should see 9 passed tests (3 tests across 3 browsers: Chromium, Firefox, WebKit).

Stock filter test not passing?

If getByLabel('In Stock Only') fails, the label text on the site might be worded differently. Open the shopping page, right-click the toggle, and inspect the associated label element to find the exact text.

If HD Monitor is still visible after checking the toggle, make sure you're using check() (not click()). The check() method ensures the toggle is activated regardless of its current state.

help me debug why my Playwright stock filter test is failing

Why auto-waiting matters

Notice that none of your tests include manual sleep() or wait() calls. Playwright automatically waits for elements to appear, become visible, and stabilize before performing actions or assertions. This eliminates an entire category of flaky tests that plague other frameworks.

✔️ Awesome, I've got everything!

Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the full playwright.config.ts:

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

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

Here is the full tests/shopping.spec.ts:

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

test.describe('Product Search and Filter', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/shopping');
  });

  test('should display 10 products on initial load', async ({ page }) => {
    const productCards = page.locator('[data-testid="product-card"]');
    await expect(productCards).toHaveCount(10);
  });

  test('should filter products by search term', async ({ page }) => {
    const searchInput = page.getByPlaceholder('Search products...');
    await searchInput.fill('Keyboard');

    const productCards = page.locator('[data-testid="product-card"]');
    await expect(productCards).toHaveCount(1);
    await expect(page.getByText('Mechanical Keyboard')).toBeVisible();
  });

  test('should hide out-of-stock items when filter is toggled', async ({ page }) => {
    const inStockToggle = page.getByLabel('In Stock Only');
    await inStockToggle.check();

    await expect(page.getByText('HD Monitor')).not.toBeVisible();
  });
});

You now have three working tests that verify product display, search, and filtering. Next up, you'll write tests for the cart and checkout flow to cover the full shopping experience.

Write Cart and Checkout Tests

Your search and filter tests prove the shopping page displays and narrows products correctly. Now you need to verify the most critical e-commerce flow: adding items to a cart and checking the totals.

Cart operations involve multiple user interactions chained together. A single test might click a button, wait for a badge to update, open a modal, and assert prices. Playwright's auto-waiting handles the timing between each action so your tests stay reliable without manual sleep calls.

In this step, get ready to:
  • Write a test that adds a product to the cart and verifies the cart count badge.
  • Write a test that adds multiple products and verifies item names, prices, and subtotal in the cart modal.
  • Write a test that confirms out-of-stock products cannot be added to the cart.
Add the cart count test

Your first cart test verifies the simplest interaction: clicking "Add to Cart" on a product and confirming the cart badge updates. This requires a new test.describe block dedicated to cart operations.

  • In tests/shopping.spec.ts, scroll to the bottom of the file (after the closing }); of the Product Search and Filter describe block).
  • Add the following Cart Operations describe block with a beforeEach hook and the first test:
test.describe('Cart Operations', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/shopping');
  });

  test('should update cart count when adding a product', async ({ page }) => {
    // Locate the Bluetooth Headphones card by filtering product cards
    const headphonesCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'Bluetooth Headphones' });
    await headphonesCard.getByRole('button', { name: 'Add to Cart' }).click();

    // Assert the cart badge shows 1 item
    const cartCount = page.locator('[data-testid="cart-count"]');
    await expect(cartCount).toHaveText('1');
  });
});

What does this code do?

The .filter({ hasText: 'Bluetooth Headphones' }) method narrows down from all product cards to just the one containing that text. This is called locator chaining. You start broad and filter to be specific.

Once you have the specific card, getByRole('button', { name: 'Add to Cart' }) finds the button within that card only. Playwright auto-waits for the button to be clickable before proceeding.

What are data-testid selectors?

The data-testid attribute is a convention where developers add dedicated test hooks to HTML elements. These are more stable than CSS classes because they won't change when styling updates happen.

If a site doesn't have data-testid attributes, you can fall back to role-based locators like getByRole() or text-based locators like getByText().

  • Save tests/shopping.spec.ts.
  • Run the tests by executing this command:
npx playwright test tests/shopping.spec.ts --project=chromium

You should see 4 tests passing (3 search/filter tests plus your new cart count test).

Test failing with a timeout?

If the test times out waiting for the cart count, the data-testid="cart-count" selector might not match the actual site. Open the site in your browser, right-click the cart badge, and select **Inspect** to check the actual attribute name.

If the site uses a different attribute, update your locator to match what you see in the DOM.

Help me debug why my Playwright cart count test is timing out

Test cart items and totals

A real checkout flow involves multiple products. This test adds two items, opens the cart modal, and verifies that names, individual prices, and the calculated subtotal all display correctly.

  • In tests/shopping.spec.ts, find the closing }); of the cart count test. Add this new test inside the Cart Operations describe block, before the final });:
  test('should display correct items and totals in cart', async ({ page }) => {
    // Add Bluetooth Headphones to cart
    const headphonesCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'Bluetooth Headphones' });
    await headphonesCard.getByRole('button', { name: 'Add to Cart' }).click();

    // Add Mechanical Keyboard to cart
    const keyboardCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'Mechanical Keyboard' });
    await keyboardCard.getByRole('button', { name: 'Add to Cart' }).click();

    // Open the cart modal
    await page.locator('[data-testid="cart-icon"]').click();

    // Verify both product names and prices are visible
    await expect(page.getByText('Bluetooth Headphones')).toBeVisible();
    await expect(page.getByText('Mechanical Keyboard')).toBeVisible();
    await expect(page.getByText('$59.99')).toBeVisible();
    await expect(page.getByText('$89.99')).toBeVisible();

    // Verify the subtotal calculation is correct
    const subtotal = page.locator('[data-testid="cart-subtotal"]');
    await expect(subtotal).toContainText('$149.98');
  });

What does this code do?

This test chains multiple actions: adding two products, then opening the cart. Playwright waits for each action to complete before moving to the next one.

The toContainText('$149.98') assertion checks that the subtotal element contains that string somewhere. This is more flexible than toHaveText() because the element might also include a label like "Subtotal: $149.98".

  • Save tests/shopping.spec.ts.
  • Run the tests again:
npx playwright test tests/shopping.spec.ts --project=chromium

You should see 5 tests passing now.

Cart modal not opening?

If the test fails when clicking the cart icon, the data-testid="cart-icon" selector might not exist on the page. Right-click the cart icon in your browser and inspect it to find the actual attribute or use a role-based locator instead.

If the price assertions fail, the site might format prices differently. Check the actual displayed text by running in headed mode:

npx playwright test tests/shopping.spec.ts --project=chromium --headed

Help me debug why my Playwright cart modal test is failing

Test the out-of-stock button state

A well-tested e-commerce site prevents users from adding out-of-stock items. This test verifies that the button is truly disabled at the HTML level, not just styled to look inactive.

  • Add this final test inside the Cart Operations describe block, after the totals test and before the closing });:
  test('should disable Add to Cart for out-of-stock products', async ({ page }) => {
    // Locate the HD Monitor card (which is out of stock)
    const monitorCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'HD Monitor' });
    const outOfStockButton = monitorCard.getByRole('button', { name: 'Out of Stock' });

    // Assert the button is disabled so users cannot add it
    await expect(outOfStockButton).toBeDisabled();
  });

What does toBeDisabled() check?

The toBeDisabled() assertion checks for the HTML disabled attribute on the element. This catches real accessibility issues where a button looks disabled but is actually still clickable.

  • Save tests/shopping.spec.ts.
  • Run the full test suite across all three browsers:
npx playwright test tests/shopping.spec.ts

You should see 6 tests passing across Chromium, Firefox, and WebKit (18 total test runs).

Button not recognized as disabled?

If the assertion fails, the button text might differ from "Out of Stock". Run the test in headed mode and inspect the actual button text and attributes.

Some sites disable buttons with CSS or JavaScript instead of the HTML disabled attribute. If that's the case, you may need to check for an aria-disabled="true" attribute instead.

Help me debug why my Playwright toBeDisabled assertion is failing

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete tests/shopping.spec.ts file with all 6 tests:

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

test.describe('Product Search and Filter', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/shopping');
  });

  test('should display 10 products on initial load', async ({ page }) => {
    const productCards = page.locator('[data-testid="product-card"]');
    await expect(productCards).toHaveCount(10);
  });

  test('should filter products by search term', async ({ page }) => {
    const searchInput = page.getByPlaceholder('Search products...');
    await searchInput.fill('Keyboard');

    const productCards = page.locator('[data-testid="product-card"]');
    await expect(productCards).toHaveCount(1);
    await expect(page.getByText('Mechanical Keyboard')).toBeVisible();
  });

  test('should hide out-of-stock items when filter is toggled', async ({ page }) => {
    const inStockToggle = page.getByLabel('In Stock Only');
    await inStockToggle.check();

    await expect(page.getByText('HD Monitor')).not.toBeVisible();
  });
});

test.describe('Cart Operations', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/shopping');
  });

  test('should update cart count when adding a product', async ({ page }) => {
    const headphonesCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'Bluetooth Headphones' });
    await headphonesCard.getByRole('button', { name: 'Add to Cart' }).click();

    const cartCount = page.locator('[data-testid="cart-count"]');
    await expect(cartCount).toHaveText('1');
  });

  test('should display correct items and totals in cart', async ({ page }) => {
    const headphonesCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'Bluetooth Headphones' });
    await headphonesCard.getByRole('button', { name: 'Add to Cart' }).click();

    const keyboardCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'Mechanical Keyboard' });
    await keyboardCard.getByRole('button', { name: 'Add to Cart' }).click();

    await page.locator('[data-testid="cart-icon"]').click();

    await expect(page.getByText('Bluetooth Headphones')).toBeVisible();
    await expect(page.getByText('Mechanical Keyboard')).toBeVisible();
    await expect(page.getByText('$59.99')).toBeVisible();
    await expect(page.getByText('$89.99')).toBeVisible();

    const subtotal = page.locator('[data-testid="cart-subtotal"]');
    await expect(subtotal).toContainText('$149.98');
  });

  test('should disable Add to Cart for out-of-stock products', async ({ page }) => {
    const monitorCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'HD Monitor' });
    const outOfStockButton = monitorCard.getByRole('button', { name: 'Out of Stock' });

    await expect(outOfStockButton).toBeDisabled();
  });
});

You now have a solid test suite covering both browsing and purchasing flows. Next up, you will configure Playwright to run these same tests across multiple browser engines and mobile device viewports automatically.

Configure Cross-Browser Testing

Your test suite now covers product search, filtering, and cart operations. All 6 tests pass across the three desktop browsers.

Desktop browsers are only half the picture. Real users browse on phones and tablets, where smaller viewports and touch interactions can break layouts that look perfect on a monitor. In this step, you'll add mobile device emulation to your Playwright config so the same tests run on iPhone and Android viewports too.

In this step, get ready to:
  • Add mobile device emulation projects (iPhone 14 and Pixel 7) to your Playwright config.
  • Run all 6 tests across 5 browser/device projects (30 total test runs).
  • Examine the HTML report to see results grouped by browser and device.
Add mobile device emulation projects

Playwright includes built-in device descriptors that bundle viewport size, user agent string, and device pixel ratio into a single profile. You don't need a real phone. Playwright emulates the mobile browser by applying these settings to its browser engine.

What is device emulation?

Playwright sets the viewport dimensions, pixel density, user agent, and touch capabilities to match a specific device. The browser engine then renders the page exactly as it would on that phone or tablet.

This catches responsive layout bugs (collapsed navbars, overlapping elements, hidden content) without needing physical hardware.

  • In VS Code, open playwright.config.ts.
  • Find this line near the top of the config:
retries: 0,
  • Change 0 to 1, so the line now looks like this:
retries: 1,

Why enable retries?

Mobile emulation tests can occasionally be flaky due to viewport-specific timing differences. Setting retries to 1 gives each failing test one more chance before it's marked as a failure. This also enables trace recording on retry, which you'll use for debugging in the next step.

  • Now scroll down to the projects array. Find the closing brace of the webkit project entry:
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  • Add the following two mobile device projects directly below the webkit entry:
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 7'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 14'] },
    },

What does this code do?

Each project entry names a browser configuration that Playwright runs your tests against.

  • devices['Pixel 7'] sets viewport to 412x915, device pixel ratio to 2.625, and user agent to a Chrome Android string.
  • devices['iPhone 14'] sets viewport to 390x844, device pixel ratio to 3, and user agent to a Safari iOS string.
  • Both enable touch event simulation automatically.
  • Save playwright.config.ts.

Seeing a syntax error?

Make sure each project entry has a trailing comma after its closing brace. Also confirm that the new entries sit inside the projects: [...] array, not outside it.

Help me fix a syntax error in my playwright.config.ts after adding mobile device projects

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the full playwright.config.ts with all 5 projects configured:

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: 1,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'https://practiceautomatedtesting.com',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 7'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 14'] },
    },
  ],
});
Run the full test suite across all 5 projects

Your config now defines 5 projects. Playwright will run all 6 tests against each project, giving you 30 total test executions.

  • Run the full test suite across all configured browsers and devices by running this command:
npx playwright test

This takes longer than before because Playwright now launches 5 browser contexts instead of 3. You should see output showing 30 tests running (6 tests x 5 projects).

What should I see?

The terminal output shows test results grouped by project name. You'll see lines like [chromium], [firefox], [webkit], [Mobile Chrome], and [Mobile Safari] prefixes next to each test name. All 30 should pass.

Some mobile tests failing?

If tests pass on desktop browsers but fail on mobile viewports, the website might render elements differently at smaller screen sizes. Check the error message to see which locator couldn't find its element.

If all tests fail, confirm your internet connection is active and that practiceautomatedtesting.com is accessible in your browser.

Help me debug why my Playwright tests pass on desktop but fail on mobile emulation projects

Open the HTML report

The HTML report gives you a visual breakdown of results per browser and device. This is where you'd spot cross-browser inconsistencies in a real project.

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

Your default browser opens with the Playwright HTML Reporter. You should see all 30 test results listed, each prefixed with its project name (chromium, firefox, webkit, Mobile Chrome, Mobile Safari).

  • Click on any test row to expand it and see execution details like duration and browser used.
  • Compare the execution times between desktop and mobile projects to see if mobile tests take longer.

Why cross-browser reports matter

In production, cross-browser reports reveal which browsers cause the most failures. A test that passes on Chromium but fails on WebKit often points to a CSS or JavaScript compatibility issue that only affects Safari users.

The project names in the report (Mobile Chrome, Mobile Safari) make it easy to filter and identify device-specific regressions before they reach users.

Report not opening?

If the browser doesn't open automatically, check the terminal output for a localhost URL (usually http://localhost:9323) and open it manually.

Help me open the Playwright HTML report

You now have a test suite that proves your e-commerce tests work across 3 desktop browsers and 2 mobile viewports. Next up, you'll learn how to debug failures visually using Playwright's Trace Viewer.

Debug with Trace Viewer

In the previous step, you ran your tests across 5 different browser and device projects and saw results grouped by project in the HTML report.

Knowing a test failed is only half the story. You need to understand why it failed. Playwright's Trace Viewer records every action your test takes and lets you replay it step-by-step with screenshots, DOM snapshots, and network requests.

In this step, get ready to:
  • Confirm trace recording is configured in your project.
  • Intentionally break a test and use Trace Viewer to debug it.
  • Fix the test and run a final clean suite across all 5 projects.
Confirm trace recording is enabled

Trace recording captures a detailed timeline of everything that happens during a test run. Your playwright.config.ts already has the two settings that make this work.

  • Open playwright.config.ts in VS Code.
  • Confirm you see these two settings in the file:
  retries: 1,

And inside the use block:

    trace: 'on-first-retry',

What do these settings do together?

The retries: 1 setting tells Playwright to automatically re-run any failing test once. The trace: 'on-first-retry' setting records a full trace during that retry attempt.

This combination is ideal because traces are only generated when something goes wrong. You get debugging data exactly when you need it, without slowing down passing tests.

Break a test and inspect the trace

To see Trace Viewer in action, you need a failing test. You will intentionally break an assertion so Playwright retries it and generates a trace file.

  • In tests/shopping.spec.ts, find the first test inside the Product Search and Filter describe block.
  • Find this line:
    await expect(productCards).toHaveCount(10);
  • Change 10 to 99, so the line now looks like this:
    await expect(productCards).toHaveCount(99);
  • Save tests/shopping.spec.ts.
  • Run the tests by running this command:
npx playwright test

You should see the "should display 10 products on initial load" test fail across all 5 projects. Playwright retries each failure once, generating trace files during the retry.

What just happened?

The test expected 99 product cards but only found 10 on the page. On the first run, it failed normally. On the retry, Playwright recorded every action, screenshot, and network request into a trace.zip file stored in the test-results/ folder.

Now open the Trace Viewer to inspect the failure visually.

  • Open the HTML report by running:
npx playwright show-report
  • In the report, click on the failed test "should display 10 products on initial load" for any browser project.
  • Click the Traces tab to see the retry trace.

You can also open a trace file directly from the terminal. Find the trace file in your test-results/ folder and open it by running:

npx playwright show-trace test-results/shopping-Product-Search-and-Filter-should-display-10-products-on-initial-load-chromium-retry1/trace.zip

Your folder name may differ

The exact folder name inside test-results/ is generated from the test name and browser project. Look for any folder ending in -retry1 and open the trace.zip file inside it.

The Trace Viewer opens in your browser. Explore these panels:

  • The Actions panel on the left shows each test step in order. Click any action to jump to that moment in time.
  • The Screenshots panel shows exactly what the browser rendered at each step.
  • The Network tab shows every HTTP request the page made.
  • The DOM Snapshot shows the full page HTML at each point, so you can inspect element attributes and structure.

Why is Trace Viewer so powerful?

Traditional debugging requires you to re-run a test and guess where things went wrong. Trace Viewer is like a time machine. You can scrub through every moment of the test execution without re-running anything.

This is especially valuable for cross-browser issues where a test passes in Chromium but fails in WebKit. You can compare the traces side-by-side to spot rendering or timing differences.

Don't see a trace file?

Make sure retries: 1 is set in your config. Traces with on-first-retry are only generated when a test retries. If retries is 0, no trace is recorded.

Also check that your test actually fails. If you forgot to save the file after changing 10 to 99, all tests pass and no retry happens.

Why isn't my trace file appearing after running tests?

Fix the test and run the final clean suite

Now that you have seen how Trace Viewer works, restore the correct assertion and confirm your entire test suite passes cleanly.

  • In tests/shopping.spec.ts, find this line:
    await expect(productCards).toHaveCount(99);
  • Change 99 back to 10, so the line looks like this:
    await expect(productCards).toHaveCount(10);
  • Save tests/shopping.spec.ts.
  • Run the full test suite one final time by running:
npx playwright test

You should see all 6 tests pass across all 5 browser/device projects. That is 30 total test runs, all green.

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

The report should show all tests passing across Chromium, Firefox, WebKit, Mobile Chrome (Pixel 7), and Mobile Safari (iPhone 14).

Still seeing failures after fixing the assertion?

Double check that you changed 99 back to exactly 10 and saved the file. If a different test is failing, it may be a network timeout. Run the tests again to confirm.

My tests are still failing after I restored the original assertion. What should I check?

✔️ Awesome, I've got everything!

Great. Double check you have saved both files.

ⓧ I'd like to double check the full code

Here is your complete playwright.config.ts:

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: 1,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'https://practiceautomatedtesting.com',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 7'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 14'] },
    },
  ],
});

And here is your complete tests/shopping.spec.ts:

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

test.describe('Product Search and Filter', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/shopping');
  });

  test('should display 10 products on initial load', async ({ page }) => {
    const productCards = page.locator('[data-testid="product-card"]');
    await expect(productCards).toHaveCount(10);
  });

  test('should filter products by search term', async ({ page }) => {
    const searchInput = page.getByPlaceholder('Search products...');
    await searchInput.fill('Keyboard');

    const productCards = page.locator('[data-testid="product-card"]');
    await expect(productCards).toHaveCount(1);
    await expect(page.getByText('Mechanical Keyboard')).toBeVisible();
  });

  test('should hide out-of-stock items when filter is toggled', async ({ page }) => {
    const inStockToggle = page.getByLabel('In Stock Only');
    await inStockToggle.check();

    await expect(page.getByText('HD Monitor')).not.toBeVisible();
  });
});

test.describe('Cart Operations', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/shopping');
  });

  test('should update cart count when adding a product', async ({ page }) => {
    const headphonesCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'Bluetooth Headphones' });
    await headphonesCard.getByRole('button', { name: 'Add to Cart' }).click();

    const cartCount = page.locator('[data-testid="cart-count"]');
    await expect(cartCount).toHaveText('1');
  });

  test('should display correct items and totals in cart', async ({ page }) => {
    const headphonesCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'Bluetooth Headphones' });
    await headphonesCard.getByRole('button', { name: 'Add to Cart' }).click();

    const keyboardCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'Mechanical Keyboard' });
    await keyboardCard.getByRole('button', { name: 'Add to Cart' }).click();

    await page.locator('[data-testid="cart-icon"]').click();

    await expect(page.getByText('Bluetooth Headphones')).toBeVisible();
    await expect(page.getByText('Mechanical Keyboard')).toBeVisible();
    await expect(page.getByText('$59.99')).toBeVisible();
    await expect(page.getByText('$89.99')).toBeVisible();

    const subtotal = page.locator('[data-testid="cart-subtotal"]');
    await expect(subtotal).toContainText('$149.98');
  });

  test('should disable Add to Cart for out-of-stock products', async ({ page }) => {
    const monitorCard = page.locator('[data-testid="product-card"]').filter({ hasText: 'HD Monitor' });
    const outOfStockButton = monitorCard.getByRole('button', { name: 'Out of Stock' });

    await expect(outOfStockButton).toBeDisabled();
  });
});

Secret mission

Add Visual Regression Testing with Screenshots

Your tests prove the site works functionally. But what about visual bugs? A button that shifts 20 pixels to the left, text that overflows its container, or a nav bar that collapses differently on Safari. Add screenshot comparison testing to catch visual differences that functional tests miss entirely.

Clean Up Your Resources

Clean Up Your Resources

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

Resources you used:

  • Playwright project folder (including node_modules, test files, and config).
  • Playwright browsers (Chromium, Firefox, WebKit) installed locally by Playwright.
  • HTML test reports in the playwright-report folder.
  • Trace files generated during test retries.

Keep everything running

No action needed. Choose this if you plan to keep building on your test suite or want to reference it for future projects.

  • Your project folder stays on your Desktop with all tests and config intact.
  • Playwright browsers remain installed and ready to run tests at any time.
  • HTML reports and trace files remain available for review.

Pause - I'll come back to this later

Free up disk space by removing bulky artifacts while keeping your test code intact for later.

  • Delete the playwright-report folder to clear old HTML reports.
  • Delete the test-results folder to remove trace files and screenshots.
  • Optionally delete node_modules to reclaim significant disk space. You can restore it later by running npm install in the project folder.
  • Keep your tests/ folder and playwright.config.ts so you can pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. This cannot be undone.

  • Delete the entire project folder from your Desktop.
  • Uninstall the Playwright browsers by running npx playwright uninstall --all from any terminal. This removes Chromium, Firefox, and WebKit browser binaries from your system.
  • Optionally uninstall Node.js if you no longer need it for other projects.

Nice Work!

Nice Work!

You just built a professional cross-browser and responsive test automation suite using Playwright. This suite proves you can automate web testing across desktop and mobile viewports with a single framework.

You've learned how to:

  • Write end-to-end tests for search, filtering, and cart operations on a real e-commerce site using TypeScript.
  • Run the same tests across 3 desktop browsers (Chromium, Firefox, WebKit) and 2 mobile viewports (iPhone 14, Pixel 7) using device emulation.
  • Debug test failures visually using the HTML Reporter and Trace Viewer with step-by-step screenshots, DOM snapshots, and network activity.
  • Add visual regression testing to catch rendering differences across browsers using screenshot comparisons.

Ready to quiz yourself?