Network Interception and API Mocking in Playwright (page.route)

With network interception, Playwright can fake API responses, simulate server errors, block ads and record traffic. In this lesson you will learn page.route step by step with examples on Playwright’s official API-mocking demo page.

📖 What You’ll Learn: what network interception is, page.route patterns, mocking responses with fulfill, modifying real responses with route.fetch, simulating errors and slow APIs, changing requests, blocking resources, request and response events, context.route, HAR record and replay, and when to mock.


🛰️ What Is Network Interception?

Every modern web page talks to a backend through network requests: fetching products, submitting forms, loading images. Playwright can sit between the browser and the network and:

  • Observe requests and responses (what was called, with which data).
  • Mock responses: return your own data instead of calling the real server.
  • Modify real responses before the page sees them.
  • Block requests you do not need, such as ads, analytics or large images.

This lets you test situations that are hard to create for real: empty lists, server errors, slow APIs, or a feature whose backend is not finished yet.


🧭 page.route(): The Core API

await page.route(urlPattern, async route => {
  // decide what to do with each matching request:
  // route.fulfill(...)  → respond with mock data
  // route.continue(...) → send it to the real server (optionally modified)
  // route.abort()       → block it
});

The URL pattern can be a glob ('**/api/products'), a regular expression (/\/api\/v\d\/fruits/) or a function. Register routes before the page makes the request, usually before page.goto().


🍓 Mock an API Response

Playwright’s official demo page https://demo.playwright.dev/api-mocking loads a list of fruits from /api/v1/fruits. Replace the real list with your own:

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

test('shows mocked fruit', async ({ page }) => {
  await page.route('*/**/api/v1/fruits', async route => {
    const json = [{ name: 'Strawberry', id: 21 }];
    await route.fulfill({ json });              // status 200 with a JSON body
  });

  await page.goto('https://demo.playwright.dev/api-mocking');
  await expect(page.getByText('Strawberry')).toBeVisible();
});

route.fulfill() options:

OptionPurpose
jsonBody as an object, sets the JSON content type automatically
statusHTTP status code, e.g. 500
bodyRaw string or Buffer body
contentTypee.g. 'text/html'
headersResponse headers
pathServe the body from a local file

✏️ Modify a Real Response

Fetch the real response, change it, then return it. This keeps the test close to reality while controlling one detail:

test('adds a fruit to the real list', async ({ page }) => {
  await page.route('*/**/api/v1/fruits', async route => {
    const response = await route.fetch();       // real request
    const json = await response.json();
    json.push({ name: 'Loquat', id: 100 });     // add one item
    await route.fulfill({ response, json });    // keep real status and headers
  });

  await page.goto('https://demo.playwright.dev/api-mocking');
  await expect(page.getByText('Loquat', { exact: true })).toBeVisible();
});

💥 Simulate Errors, Empty Data and Slow Responses

Use mocks to test how your application handles problems. Point these at your own app’s endpoints and assert its error or empty states:

// Server error
await page.route('**/api/orders', route => route.fulfill({ status: 500, body: 'Internal Server Error' }));

// Empty list
await page.route('**/api/orders', route => route.fulfill({ json: [] }));

// Network failure
await page.route('**/api/orders', route => route.abort('failed'));

// Slow API: wait 3 seconds before continuing to the real server
await page.route('**/api/orders', async route => {
  await new Promise(resolve => setTimeout(resolve, 3000));
  await route.continue();
});

💡 Tip: Error-state tests are where mocking shines. Ask: what should the user see when the API returns 500, an empty array, or takes 10 seconds? Then write a test for each.


🔧 Modify Outgoing Requests

route.continue() can change the request before it reaches the server:

await page.route('**/api/**', async route => {
  const headers = { ...route.request().headers(), 'x-test-run': 'playwright' };
  await route.continue({ headers });
});

You can also change method, postData or url, for example to point one endpoint at a staging server.


🚫 Block Unneeded Resources

Blocking images, fonts or third-party scripts can make tests faster and more stable:

test.beforeEach(async ({ page }) => {
  await page.route('**/*.{png,jpg,jpeg,gif,svg,webp}', route => route.abort());
  await page.route(/google-analytics|googletagmanager|doubleclick/, route => route.abort());
});

⚠️ Common mistake: Do not block resources in tests that check layout or images. Blocking is for functional tests where those resources do not matter.


👂 Listen to Requests and Responses

To observe traffic without changing it, use events:

page.on('request', request => console.log('>>', request.method(), request.url()));
page.on('response', response => console.log('<<', response.status(), response.url()));
page.on('requestfailed', request => console.log('FAILED', request.url(), request.failure()?.errorText));

To wait for one specific call and assert its data, use page.waitForResponse(), explained in Playwright waits:

const responsePromise = page.waitForResponse('**/api/v1/fruits');
await page.goto('https://demo.playwright.dev/api-mocking');
const response = await responsePromise;
expect(response.status()).toBe(200);

🌍 Routes for All Pages: context.route()

page.route() applies to one tab. context.route() applies to every page in the browser context, including popups and new tabs:

test('mock for every tab', async ({ context, page }) => {
  await context.route('**/api/config', route => route.fulfill({ json: { featureX: true } }));
  await page.goto('https://example.com');
});

Remove a route with page.unroute(pattern) or page.unrouteAll().


📼 Record and Replay with HAR Files

A HAR file is a recording of network traffic. Record real responses once, then replay them in tests so they do not depend on the backend:

// First run: record (update: true) the API traffic into a HAR file
await page.routeFromHAR('./hars/fruits.har', { url: '*/**/api/v1/fruits', update: true });
await page.goto('https://demo.playwright.dev/api-mocking');

// Later runs: remove update: true to replay the saved responses
// await page.routeFromHAR('./hars/fruits.har', { url: '*/**/api/v1/fruits' });

⚖️ When to Mock (and When Not To)

Mock itUse the real backend
Error, empty and edge-case statesCritical end-to-end business flows
Third-party services (payments, maps, SMS)Smoke tests before a release
Features whose backend is not readyContract between frontend and backend
Slow or rate-limited APIsData that must really be saved

A healthy suite has both: a few full end-to-end tests against real services, and many focused tests that mock the network.


📝 Assignment

Use https://demo.playwright.dev/api-mocking:

  1. Mock /api/v1/fruits to return three fruits of your choice and assert all three are visible.
  2. Modify the real response by adding a fruit with route.fetch() and assert it appears.
  3. Mock an empty array and describe what the page shows.
  4. Log every request and response URL while the page loads.
  5. Use page.waitForResponse to assert the fruits API returns status 200.
  6. On https://www.saucedemo.com block all images with route.abort() and log in; confirm the test still passes.
  7. Bonus: record the fruits API into a HAR file and replay it in a second test.

❓ FAQ

🤔 How do I mock an API response in Playwright?

Register a route before navigation and fulfill it: await page.route('**/api/items', route => route.fulfill({ json: [...] })).

🤔 What is the difference between route.fulfill, route.continue and route.abort?

fulfill answers the request with your own response, continue sends it to the real server (optionally modified), and abort blocks it.

🤔 How do I modify a real API response in Playwright?

Call const response = await route.fetch(), change the JSON, then route.fulfill({ response, json }).

🤔 How do I block images or ads in Playwright?

Use a route with route.abort(), for example page.route('**/*.{png,jpg}', r => r.abort()).

🤔 Does page.route work for API tests with the request fixture?

No. Routes intercept traffic from the browser page. Requests sent with the request fixture go directly to the server; see API testing with Playwright.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Mobile Device Emulation in Playwright: iPhone, Android and Responsive Testing

Most of your users are on phones, and Playwright can test your site as an iPhone, Android phone or tablet without any real device. In this lesson you will add mobile projects, write mobile-aware tests, check responsive breakpoints and emulate location, language and offline mode.

📖 What You’ll Learn: what device emulation changes, device descriptors and mobile projects, isMobile, test.use for a single file, tap vs click, responsive breakpoint tests, geolocation, locale, time zone, dark mode and offline emulation, and a mobile testing checklist.


📱 What Is Mobile Emulation?

More than half of web traffic comes from phones. Mobile emulation lets Playwright open your website the way a phone does: small screen, touch input, mobile user agent and high pixel density. It runs in the normal desktop browser engines, so it is fast and needs no real device or Android emulator.

Emulation changes:

PropertyExample (iPhone 13)
viewport390 x 664 CSS pixels
deviceScaleFactor3 (retina)
isMobiletrue (mobile meta viewport is respected)
hasTouchtrue (touch events, tap())
userAgentAn iPhone Safari user agent
defaultBrowserTypewebkit

📝 Note: Emulation tests mobile websites, not native apps. For Android and iOS apps from the app stores, use Appium. For final sign-off on real hardware, use a real-device cloud.


🧭 Add Mobile Projects to the Config

Playwright ships ready-made device descriptors for popular phones and tablets. Add them as projects in playwright.config.ts:

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

export default defineConfig({
  use: { baseURL: 'https://www.saucedemo.com' },
  projects: [
    { name: 'Desktop Chrome', use: { ...devices['Desktop Chrome'] } },
    { name: 'Mobile Chrome',  use: { ...devices['Pixel 5'] } },
    { name: 'Mobile Safari',  use: { ...devices['iPhone 13'] } },
    { name: 'iPad',           use: { ...devices['iPad (gen 7)'] } },
    { name: 'iPhone landscape', use: { ...devices['iPhone 13 landscape'] } },
  ],
});
npx playwright test --project="Mobile Safari"

The full list of device names is in Playwright’s source file deviceDescriptorsSource.json; your editor also autocompletes the names inside devices['...'].


✍️ A Mobile Test

The same test code runs on desktop and mobile. Where behaviour differs, check isMobile:

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

test('logged-in user can open the side menu', async ({ page, isMobile }) => {
  await page.goto('/');
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();

  await page.locator('#react-burger-menu-btn').click();
  await expect(page.locator('#logout_sidebar_link')).toBeVisible();

  if (isMobile) {
    console.log('Viewport on this device:', page.viewportSize());
  }
});

🎯 Emulate a Device in One File: test.use

To make one file always run as a phone, regardless of the project:

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

test.use({ ...devices['Pixel 5'] });   // must be at the top level of the file

test('mobile login page', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await expect(page.getByPlaceholder('Username')).toBeVisible();
});

⚠️ Common mistake: Device descriptors include defaultBrowserType, which cannot be changed inside a test.describe block. If you see “Cannot use({ defaultBrowserType }) in a describe group”, move test.use to the top of the file or remove defaultBrowserType from the object.


👆 Touch: tap() vs click()

With hasTouch: true, you can send real touch events:

await page.getByRole('button', { name: 'Login' }).tap();

click() also works on mobile emulation; use tap() when the page listens specifically for touch events (swipe menus, mobile-only gestures).


📐 Test Responsive Breakpoints with Viewports

You do not always need a full device. To check how a layout responds at different widths, set the viewport directly and generate tests with a loop (parameterized tests):

const breakpoints = [
  { name: 'mobile',  width: 375,  height: 812 },
  { name: 'tablet',  width: 768,  height: 1024 },
  { name: 'desktop', width: 1440, height: 900 },
];

for (const bp of breakpoints) {
  test('login form is visible on ' + bp.name, async ({ page }) => {
    await page.setViewportSize({ width: bp.width, height: bp.height });
    await page.goto('https://www.saucedemo.com/');
    await expect(page.locator('.login-box')).toBeVisible();
    await expect(page).toHaveScreenshot('login-' + bp.name + '.png');
  });
}

Combine this with visual regression testing to catch broken responsive layouts.


🌍 Emulate Location, Language, Time Zone, Theme and Offline

Mobile scenarios often depend on context, all of which Playwright can emulate:

test.use({
  geolocation: { latitude: 19.0760, longitude: 72.8777 },  // Mumbai
  permissions: ['geolocation'],
  locale: 'hi-IN',
  timezoneId: 'Asia/Kolkata',
  colorScheme: 'dark',
});

test('shows the nearest store', async ({ page, context }) => {
  await page.goto('https://example.com/store-locator');
  // ...

  await context.setOffline(true);    // simulate losing the network
  await page.reload().catch(() => {}); // the reload fails while offline
  await context.setOffline(false);
});
OptionUse case
geolocation + permissionsStore locators, delivery areas, maps
localeTranslated UI, number and date formats
timezoneIdDates and deadlines across time zones
colorSchemeDark mode styles
context.setOffline(true)Offline banners, retry logic, PWAs

✅ Mobile Testing Checklist

  • Key user journeys pass on at least one Android (Pixel) and one iOS (iPhone) descriptor.
  • Navigation works through the mobile menu, not only desktop links.
  • Forms are usable with the on-screen keyboard layout (no hidden buttons).
  • Tap targets are reachable; no horizontal scrolling at 375 px.
  • Visual baselines exist for the main mobile pages.

📝 Assignment

  1. Add Mobile Chrome (Pixel 5) and Mobile Safari (iPhone 13) projects and run your Sauce Demo login tests on them.
  2. Write a test that logs in and opens the side menu, and log isMobile and the viewport size.
  3. Create a file that always runs as iPad (gen 7) using test.use at the top level.
  4. Use tap() instead of click() for the login button on a touch device.
  5. Generate breakpoint tests for 375, 768 and 1440 px widths with a visual snapshot each.
  6. Bonus: emulate Mumbai geolocation, the hi-IN locale and dark mode, then log navigator.language with page.evaluate.

❓ FAQ

🤔 How do I test a mobile website with Playwright?

Add a project using a device descriptor, for example use: { ...devices['iPhone 13'] }, and run your tests with that project. Playwright emulates the screen, touch, user agent and pixel density.

🤔 Can Playwright test native Android or iOS apps?

No. Playwright tests web apps, including mobile websites through emulation. Use Appium for native apps.

🤔 What is the difference between setViewportSize and a device descriptor?

setViewportSize only changes the window size. A device descriptor also sets touch support, mobile mode, user agent and device scale factor, which affects how the site behaves.

🤔 How do I see the list of Playwright devices?

Type devices[' in your editor to get autocomplete, or look at deviceDescriptorsSource.json in the Playwright repository.

🤔 Why does test.use with a device fail inside describe?

Device descriptors contain defaultBrowserType, which can only be set at the top level of a file or in the config, not inside a describe block.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Cross-Browser Testing with Playwright Projects (Chromium, Firefox, WebKit)

One Playwright test suite can run in Chrome, Edge, Firefox and Safari’s engine without changing a single test. In this lesson you will configure browser projects, test branded browsers, handle browser-specific cases and design a cost-effective cross-browser strategy.

📖 What You’ll Learn: supported browser engines, projects and device presets, running one or several browsers, branded Chrome and Edge channels, the browserName fixture, per-browser settings, a practical CI strategy, and common cross-browser differences.


🌐 Why Cross-Browser Testing?

Your users do not all use Chrome. A layout that works in Chromium can break in Safari, a date picker can behave differently in Firefox, and a CSS feature may be missing in an older engine. Cross-browser testing runs the same tests in several browsers to catch those differences before users do.

Playwright makes this unusually easy: one test suite, one API, three browser engines.

EngineCoversNotes
ChromiumGoogle Chrome, Microsoft Edge, Opera, BraveMost used engine worldwide
FirefoxMozilla FirefoxPlaywright ships a patched Firefox build
WebKitApple Safari (macOS and iOS)Runs on Windows, Linux and macOS through Playwright’s WebKit build

🧭 Projects: One Config, Many Browsers

Browsers are configured as projects in playwright.config.ts. Each test runs once per project:

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

export default defineConfig({
  testDir: './tests',
  use: { baseURL: 'https://www.saucedemo.com' },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
  ],
});

The devices presets set a realistic viewport, user agent and device scale factor for each browser. Run the suite:

npx playwright test                                    # all three browsers
npx playwright test --project=firefox                  # one browser
npx playwright test --project=chromium --project=webkit

The HTML report groups results by project, so you can see immediately if a test fails in only one browser.


🏷️ Test Branded Browsers: Chrome and Edge

Playwright’s Chromium is open-source Chromium. To test the actual Google Chrome or Microsoft Edge your users install, use a channel:

projects: [
  { name: 'Google Chrome',  use: { ...devices['Desktop Chrome'], channel: 'chrome' } },
  { name: 'Microsoft Edge', use: { ...devices['Desktop Edge'],   channel: 'msedge' } },
],

If they are not installed on the machine:

npx playwright install chrome
npx playwright install msedge

📝 Note: Branded Safari cannot be automated by Playwright. The WebKit project is the closest equivalent and catches the vast majority of Safari rendering and behaviour issues.


🔧 Browser-Specific Code: browserName

Sometimes a feature does not exist in one browser, or a known browser bug needs a workaround. The browserName fixture tells you which browser is running:

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

test('export button works', async ({ page, browserName }) => {
  test.skip(browserName === 'webkit', 'Export is not supported in WebKit yet, see ticket QA-101');
  // ...
});

test('log the current browser', async ({ browserName }) => {
  console.log('Running in ' + browserName); // 'chromium' | 'firefox' | 'webkit'
});

You can also skip or configure a whole file for one browser:

test.skip(({ browserName }) => browserName === 'firefox', 'Feature not supported in Firefox yet');

⚠️ Common mistake: Keep browser-specific branches rare and always give a reason. If many tests need them, the application probably has a real cross-browser bug that should be reported.


⚙️ Different Settings per Browser

Each project can override any use option, and can include or exclude tests:

projects: [
  { name: 'chromium', use: { ...devices['Desktop Chrome'], viewport: { width: 1920, height: 1080 } } },
  { name: 'firefox',  use: { ...devices['Desktop Firefox'] }, testIgnore: /.*visual\.spec\.ts/ },
  { name: 'webkit',   use: { ...devices['Desktop Safari'] }, retries: 2 },
],

Here visual tests are skipped in Firefox (one baseline set is enough, see visual testing), and WebKit gets extra retries while a known issue is investigated.


🚀 A Practical Cross-Browser Strategy

Running every test in every browser on every commit is slow and usually unnecessary. A common, cost-effective approach:

WhenWhat runs
Every pull requestFull suite in Chromium, @smoke tests in Firefox and WebKit
NightlyFull suite in all browsers, including branded Chrome and Edge
Before a releaseFull suite in all browsers plus mobile emulation

Combine projects with tags (tags and grep):

projects: [
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  { name: 'firefox-smoke', grep: /@smoke/, use: { ...devices['Desktop Firefox'] } },
  { name: 'webkit-smoke',  grep: /@smoke/, use: { ...devices['Desktop Safari'] } },
],

In CI, split browsers across parallel jobs or machines to keep the pipeline fast; this is covered in the parallel execution and GitHub Actions lessons in Module 12.


🐞 Typical Cross-Browser Differences to Watch For

  • Fonts and layout: text wraps differently, so elements move. Prefer role and text locators over position-based ones.
  • Date and number inputs: native pickers differ; fill values instead of clicking calendar widgets.
  • Timing: WebKit and Firefox can be slower on some pages; rely on auto-waiting, never fixed sleeps.
  • Permissions and APIs: clipboard, notifications and some media APIs differ between engines.
  • Downloads and new tabs: behave the same in Playwright’s API, but always assert the result, not the browser UI.

📝 Assignment

  1. Make sure your config has Chromium, Firefox and WebKit projects, run your Sauce Demo tests in all three, and compare the timings in the HTML report.
  2. Add a Google Chrome project with channel: 'chrome' and run only that project.
  3. Log browserName in a test, then skip that test in WebKit with a clear reason.
  4. Create firefox-smoke and webkit-smoke projects that only run @smoke tests.
  5. Exclude visual tests from the Firefox project with testIgnore.
  6. Bonus: write a short “cross-browser strategy” for your project: what runs on pull requests, nightly and before releases.

❓ FAQ

🤔 How do I run Playwright tests in multiple browsers?

Define a project per browser in playwright.config.ts (Chromium, Firefox, WebKit). npx playwright test runs every test once per project.

🤔 How do I run Playwright tests in Google Chrome instead of Chromium?

Add a project with channel: 'chrome' and install Chrome with npx playwright install chrome if needed. Use channel: 'msedge' for Microsoft Edge.

🤔 Does Playwright support Safari?

Playwright tests WebKit, the engine behind Safari, on Windows, Linux and macOS. It does not drive the branded Safari application.

🤔 How do I skip a test in one browser?

Use the browserName fixture: test.skip(browserName === 'webkit', 'reason').

🤔 How do I run tests in only one browser?

Pass --project=chromium (or another project name) on the command line.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Data-Driven (Parameterized) Tests in Playwright with TypeScript

Why write the same test five times when you can write it once and feed it five data sets? In this lesson you will build data-driven Playwright tests with simple loops, typed test data, external JSON and CSV files, and project-level parameters.

📖 What You’ll Learn: what data-driven testing is, parameterizing with for…of, unique test titles, typing data with interfaces, describe blocks per data set, loading data from JSON and CSV, parameterizing projects with fixture options, and best practices.


📊 What Is Data-Driven Testing?

Data-driven (parameterized) testing means writing the test logic once and running it with many sets of data. Instead of four nearly identical login tests, you write one test and feed it four users.

Benefits:

  • Less duplicated code, so fixes happen in one place.
  • Adding a scenario means adding one line of data.
  • Each data set still appears as a separate test in the report, so you see exactly which input failed.

Playwright Test has no special “data provider” annotation. You simply generate tests with a normal loop, which makes it flexible and easy to understand.


🔁 Parameterize with a for…of Loop

All error messages below come from the Sauce Demo login page:

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

const invalidLogins = [
  { name: 'locked out user', username: 'locked_out_user', password: 'secret_sauce', error: 'Sorry, this user has been locked out.' },
  { name: 'wrong password',  username: 'standard_user',   password: 'wrong',        error: 'Username and password do not match' },
  { name: 'empty username',  username: '',                password: 'secret_sauce', error: 'Username is required' },
  { name: 'empty password',  username: 'standard_user',   password: '',             error: 'Password is required' },
];

for (const data of invalidLogins) {
  test('login fails: ' + data.name, async ({ page }) => {
    await page.goto('https://www.saucedemo.com/');
    await page.getByPlaceholder('Username').fill(data.username);
    await page.getByPlaceholder('Password').fill(data.password);
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page.locator('[data-test="error"]')).toContainText(data.error);
  });
}

Running it produces four tests:

✓ login fails: locked out user
✓ login fails: wrong password
✓ login fails: empty username
✓ login fails: empty password

⚠️ Common mistake: Every test needs a unique title. If two generated tests have the same name, Playwright reports a “duplicate test title” error. Always include a value from the data in the title.


🧾 Type Your Test Data

Give the data a type so mistakes are caught in the editor (TypeScript interfaces):

interface LoginCase {
  name: string;
  username: string;
  password: string;
  error: string;
}

const invalidLogins: LoginCase[] = [
  { name: 'locked out user', username: 'locked_out_user', password: 'secret_sauce', error: 'Sorry, this user has been locked out.' },
  // a typo such as "pasword" now shows a red underline
];

🔀 Another Example: Sorting Options

const sortCases = [
  { option: 'az',   label: 'Name (A to Z)',       firstName: 'Sauce Labs Backpack' },
  { option: 'za',   label: 'Name (Z to A)',       firstName: 'Test.allTheThings() T-Shirt (Red)' },
  { option: 'lohi', label: 'Price (low to high)', firstPrice: '$7.99' },
  { option: 'hilo', label: 'Price (high to low)', firstPrice: '$49.99' },
];

test.describe('product sorting', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://www.saucedemo.com/');
    await page.getByPlaceholder('Username').fill('standard_user');
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
  });

  for (const c of sortCases) {
    test('sort by ' + c.label, async ({ page }) => {
      await page.locator('[data-test="product-sort-container"]').selectOption(c.option);
      if (c.firstName) {
        await expect(page.locator('.inventory_item_name').first()).toHaveText(c.firstName);
      }
      if (c.firstPrice) {
        await expect(page.locator('.inventory_item_price').first()).toHaveText(c.firstPrice);
      }
    });
  }
});

📦 One describe Block per Data Set

When each data set needs several tests, generate a describe block per item:

const users = ['standard_user', 'problem_user', 'performance_glitch_user'];

for (const user of users) {
  test.describe('as ' + user, () => {
    test.beforeEach(async ({ page }) => {
      await page.goto('https://www.saucedemo.com/');
      await page.getByPlaceholder('Username').fill(user);
      await page.getByPlaceholder('Password').fill('secret_sauce');
      await page.getByRole('button', { name: 'Login' }).click();
    });

    test('sees the products page', async ({ page }) => {
      await expect(page.locator('.title')).toHaveText('Products');
    });

    test('can open the cart', async ({ page }) => {
      await page.locator('.shopping_cart_link').click();
      await expect(page).toHaveURL(/cart\.html/);
    });
  });
}

📂 Load Data from External Files

Keep large data sets outside the test file, so non-programmers can maintain them.

JSON

// test-data/invalid-logins.json → [{ "name": "...", "username": "...", "password": "...", "error": "..." }]
import invalidLogins from '../test-data/invalid-logins.json';

for (const data of invalidLogins) {
  test('login fails: ' + data.name, async ({ page }) => { /* ... */ });
}

CSV

import fs from 'fs';
import path from 'path';
import { parse } from 'csv-parse/sync';   // npm install -D csv-parse

const records: { name: string; username: string; password: string; error: string }[] =
  parse(fs.readFileSync(path.join(__dirname, '../test-data/logins.csv')), { columns: true, skip_empty_lines: true });

for (const data of records) {
  test('csv login: ' + data.name, async ({ page }) => { /* ... */ });
}

Full walkthroughs: read test data from JSON and read test data from CSV.

📝 Note: Test data used to generate tests must be available when Playwright collects the tests, so read it at the top level of the file (synchronously). Data loaded inside beforeAll is too late to create new tests.


🧩 Parameterize Whole Projects with Options

Sometimes you want to run the entire suite with different values, for example as different user roles or against different environments. Define a fixture option and set it per project:

// fixtures.ts
import { test as base } from '@playwright/test';

export type TestOptions = { role: string };

export const test = base.extend<TestOptions>({
  role: ['standard_user', { option: true }],   // default value, can be overridden in the config
});
export { expect } from '@playwright/test';
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
import type { TestOptions } from './fixtures';

export default defineConfig<TestOptions>({
  projects: [
    { name: 'standard', use: { ...devices['Desktop Chrome'], role: 'standard_user' } },
    { name: 'problem',  use: { ...devices['Desktop Chrome'], role: 'problem_user' } },
  ],
});
// tests/roles.spec.ts
import { test, expect } from '../fixtures';

test('products page loads', async ({ page, role }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill(role);
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page.locator('.title')).toHaveText('Products');
});

Each test now runs once per project, with a different role. This builds on custom fixtures.


✅ Best Practices

  • Include the distinguishing value in every generated test title.
  • Keep each data row small and focused on one scenario.
  • Type your data with an interface.
  • Put large or frequently changing data in JSON or CSV files.
  • Do not loop inside one test over many inputs: the first failure stops the remaining checks and the report shows one test instead of many.
// ❌ One test, many inputs: stops at the first failure
test('all invalid logins', async ({ page }) => {
  for (const data of invalidLogins) { /* ... */ }
});

📝 Assignment

  1. Create the four invalid-login cases above and run them; confirm the report shows four separate tests.
  2. Add an interface for the data and introduce a deliberate typo to see the editor error.
  3. Move the data into test-data/invalid-logins.json and import it.
  4. Write a parameterized test for the four sorting options.
  5. Generate a describe block per user (standard_user, problem_user) with two tests each.
  6. Bonus: create a role option with { option: true } and two projects that use different roles.

❓ FAQ

🤔 How do I write data-driven tests in Playwright?

Put your data in an array (or load it from JSON or CSV) and create a test for each item with a for...of loop, using a value from the data in the test title.

🤔 Does Playwright have a data provider like TestNG?

Not as an annotation. A normal loop that calls test() for each data item does the same job, and each item appears as a separate test in the report.

🤔 Why do I get a duplicate test title error?

Two generated tests have the same title. Include a unique value from the data, such as a name or id, in each title.

🤔 Can I read test data in beforeAll for parameterized tests?

Not for generating tests. Tests are created when the file is loaded, so read the data at the top level of the file.

🤔 How do I run the whole suite with different parameters?

Define a fixture option with { option: true } and set different values in several projects in the config.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Retries and Flaky Tests: Detect, Debug and Fix Them

Flaky tests destroy trust in automation. In this lesson you will configure retries the right way, detect unstable tests before they reach CI, debug failures with traces, and fix the root causes that make Playwright tests flaky.

📖 What You’ll Learn: what flaky tests are, configuring retries globally and per group, passed vs flaky vs failed, repeat-each and fail-on-flaky-tests, traces on retry, testInfo.retry, the common causes of flakiness with fixes, managing known flaky tests, and a retry policy checklist.


🎲 What Is a Flaky Test?

A flaky test passes sometimes and fails sometimes without any change to the code. Flaky tests are dangerous: teams stop trusting red builds, start re-running pipelines “until green”, and real bugs slip through.

Playwright gives you tools to detect, diagnose and fix flakiness. Retries are part of that toolkit, but they are a safety net, not a cure.


🔁 Configure Retries

By default Playwright does not retry failed tests. Turn retries on in playwright.config.ts:

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

export default defineConfig({
  retries: process.env.CI ? 2 : 0,   // retry twice in CI, never locally
});

Or from the command line, or for one group of tests:

npx playwright test --retries=2
test.describe('payment gateway (third-party sandbox)', () => {
  test.describe.configure({ retries: 3 });
  test('card payment', async ({ page }) => { /* ... */ });
});

How retries work

When a test fails, Playwright discards the worker process and runs the test again in a fresh one, including its beforeAll hooks. The final result is one of:

StatusMeaning
passedPassed on the first attempt
flakyFailed at least once, then passed on a retry
failedFailed on every attempt

The HTML report has a Flaky filter, and the terminal summary lists flaky tests separately. Treat every flaky test as a bug to fix.


🔍 Detect Flaky Tests Before They Reach CI

Run a test many times in a row to see whether it is stable:

npx playwright test tests/cart.spec.ts --repeat-each=10            # 10 runs per test
npx playwright test tests/cart.spec.ts --repeat-each=10 --workers=4 # also under parallel load
npx playwright test --fail-on-flaky-tests --retries=1              # fail the run if any test is flaky

💡 Tip: Run new or changed tests with --repeat-each=10 before opening a pull request. Ten passes in a row is a good first sign of stability.


🔬 Diagnose the Failure with Traces

Record a trace for the retry so you can see exactly what happened in the failed attempt:

use: {
  trace: 'on-first-retry',          // records a trace when a test is retried
  screenshot: 'only-on-failure',
  video: 'retain-on-failure',
},

Open it from the HTML report or with npx playwright show-trace. The trace shows every action, a DOM snapshot before and after, network calls and console errors, so you can compare a passing and a failing attempt. Screenshots and videos are covered in screenshots and video recording.

Inside a test you can also react to retries:

test('import report', async ({ page }, testInfo) => {
  if (testInfo.retry > 0) {
    console.log('Retry #' + testInfo.retry + ': cleaning up data from the previous attempt');
    // e.g. delete the half-imported report through the API
  }
  // ...
});

🧯 Common Causes of Flaky Tests and How to Fix Them

CauseSymptomFix
Hard waits (waitForTimeout)Passes on fast machines, fails on slow CIWait for a condition: web-first assertions, waitForResponse (waits)
Checking a value onceexpect(await el.textContent()) fails while the page is updatingUse await expect(el).toHaveText(), which retries
Missing awaitRandom failures, actions out of orderAwait every action; enable no-floating-promises (async/await)
Tests depend on each otherPass in order, fail alone or in parallelMake each test create its own data and state
Shared test dataTwo parallel tests edit the same user or recordUnique data per test, e.g. add testInfo.workerIndex or a timestamp
Unstable locatorsElement sometimes not found or wrong element clickedRole and test-id locators, no nth() on changing lists (locators)
Animations and overlays“Element intercepts pointer events”Wait for the overlay to be hidden; never use force: true as a fix
Slow or unreliable third-party servicesTimeouts on payment, maps, analyticsMock those requests with page.route (Network Mocking lesson, Module 8)
Environment differencesFails only in CISame browser versions, viewport, time zone and locale in config; run CI in Docker

Example: from flaky to stable

// ❌ Flaky: fixed sleep and a one-time check
await page.getByRole('button', { name: 'Search' }).click();
await page.waitForTimeout(2000);
expect(await page.locator('.result').count()).toBeGreaterThan(0);

// ✅ Stable: wait for the response, then a retrying assertion
const response = page.waitForResponse(r => r.url().includes('/api/search') && r.ok());
await page.getByRole('button', { name: 'Search' }).click();
await response;
await expect(page.locator('.result').first()).toBeVisible();

Example: unique test data per worker

test('register a new user', async ({ page }, testInfo) => {
  const email = 'user_' + testInfo.workerIndex + '_' + Date.now() + '@test.com';
  await page.goto('/register');
  await page.getByLabel('Email').fill(email);
  // ...
});

🏷️ Managing Known Flaky Tests

While you investigate, keep the main pipeline trustworthy:

  • Mark the test with test.fixme() and a link to the ticket, or tag it @flaky and exclude it with --grep-invert @flaky (tags and grep).
  • Give genuinely slow tests more time with test.slow() instead of raising the global timeout (annotations).
  • Track flaky tests over time; a rising count is an early warning about the app or the environment.
test('export to PDF', { tag: '@flaky' }, async ({ page }) => {
  test.fixme(true, 'Flaky in CI, see JIRA QA-123');
  // ...
});

✅ Retry Policy Checklist

  • retries: 0 locally so you notice flakiness immediately.
  • retries: 1 or 2 in CI as a safety net, with trace: 'on-first-retry'.
  • Review the flaky list in every report and fix the root cause.
  • Consider --fail-on-flaky-tests for release pipelines.
  • Never increase retries to hide a problem.

📝 Assignment

  1. Set retries: process.env.CI ? 2 : 0 and trace: 'on-first-retry' in your config.
  2. Write a test that fails randomly (for example expect(Math.random()).toBeGreaterThan(0.5)), run it with --retries=3 and find it under Flaky in the HTML report.
  3. Run one of your real Sauce Demo tests with --repeat-each=10 --workers=4 and confirm it is stable.
  4. Take this flaky snippet and rewrite it to be stable: await page.click('#load'); await page.waitForTimeout(3000); expect(await page.locator('#status').innerText()).toBe('Done');
  5. Log testInfo.retry in a test and observe its value on each attempt.
  6. Bonus: tag one test @flaky, exclude it with grepInvert in the config, and confirm it no longer runs.

❓ FAQ

🤔 How do I retry failed tests in Playwright?

Set retries in playwright.config.ts (for example retries: 2), pass --retries=2 on the command line, or use test.describe.configure({ retries: 2 }) for a group.

🤔 What does “flaky” mean in the Playwright report?

The test failed at least once and then passed on a retry. Playwright reports it separately so you can find and fix unstable tests.

🤔 How do I find flaky tests in Playwright?

Run them repeatedly with --repeat-each=10 (ideally with several workers), and check the Flaky filter in the HTML report from CI runs with retries enabled.

🤔 Should I use retries in Playwright?

Use a small number in CI as a safety net, combined with traces on retry. Keep retries at zero locally, and always fix the root cause of flaky tests.

🤔 What is the most common cause of flaky Playwright tests?

Timing: fixed sleeps, one-time checks instead of web-first assertions, and missing await. Replacing them with auto-waiting actions and retrying assertions fixes most flakiness.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Group, Tag and Filter Playwright Tests (describe, Tags and grep)

As a test suite grows, you need to run the right tests at the right time. In this lesson you will group tests with describe blocks, label them with tags, filter runs with grep, and make those filters permanent in the config and npm scripts.

📖 What You’ll Learn: test.describe and nested groups, the tag option and title tags, a practical tagging strategy, –grep and –grep-invert including AND and OR patterns, grep in config projects, npm scripts, describe.configure and serial mode, focus and skip, and reading tags at runtime.


🗂️ Why Organize Tests?

With ten tests, organization does not matter. With five hundred, it decides whether your team can answer questions like:

  • “Run only the smoke tests before every deployment.”
  • “Run the checkout tests after the payments team changed something.”
  • “Skip the slow tests on pull requests, run them nightly.”

Playwright gives you three tools for this: describe blocks to group tests, tags to label them, and grep to filter what runs.


📦 Group Tests with test.describe

test.describe groups related tests under a title. Groups can be nested, and the titles are combined in reports, for example Cart › Add items › add one product.

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

test.describe('Cart', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://www.saucedemo.com/');
    await page.getByPlaceholder('Username').fill('standard_user');
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
  });

  test.describe('Add items', () => {
    test('add one product', async ({ page }) => {
      await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
      await expect(page.locator('.shopping_cart_badge')).toHaveText('1');
    });

    test('add two products', async ({ page }) => {
      await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
      await page.locator('[data-test="add-to-cart-sauce-labs-bike-light"]').click();
      await expect(page.locator('.shopping_cart_badge')).toHaveText('2');
    });
  });

  test.describe('Remove items', () => {
    test('remove a product', async ({ page }) => {
      await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
      await page.locator('[data-test="remove-sauce-labs-backpack"]').click();
      await expect(page.locator('.shopping_cart_badge')).toHaveCount(0);
    });
  });
});

Describe blocks also scope hooks, so each group can have its own setup.


🏷️ Tag Tests

A tag is a label that starts with @. Add tags with the tag option (recommended) or directly in the title:

// Option 1: tag details (shown as tags in the HTML report)
test('user can log in', { tag: '@smoke' }, async ({ page }) => {
  // ...
});

test('full checkout flow', { tag: ['@regression', '@checkout'] }, async ({ page }) => {
  // ...
});

// Option 2: tag in the title
test('user can log out @smoke', async ({ page }) => {
  // ...
});

// Tag a whole group
test.describe('Payments', { tag: '@payments' }, () => {
  test('pay by card', async ({ page }) => { /* ... */ });
  test('pay by UPI', async ({ page }) => { /* ... */ });
});

A practical tagging strategy

TagMeaningWhen it runs
@smokeCritical paths, fastEvery commit and deployment
@regressionFull functional coverageNightly or before release
@slowLong testsNightly only
@api / @uiTest typeSeparate CI jobs
@checkout, @searchFeature areaWhen that feature changes
@flakyKnown unstable test under investigationExcluded from blocking runs

💡 Tip: Agree on a short, fixed list of tags with your team and document it. Fifty random tags are as confusing as none.


🔍 Filter Tests with –grep

--grep (short: -g) runs only tests whose full title, including describe titles and tags, matches a regular expression:

npx playwright test --grep @smoke                 # tagged @smoke
npx playwright test -g "add two products"         # by title text
npx playwright test --grep "Cart"                 # every test inside the Cart describe
npx playwright test --grep-invert @slow           # everything except @slow
npx playwright test --grep "@smoke|@api"          # @smoke OR @api
npx playwright test --grep "(?=.*@smoke)(?=.*@checkout)"   # @smoke AND @checkout
npx playwright test --grep @regression --grep-invert @flaky

📝 Note: On Windows PowerShell, keep the quotes around patterns that contain |, otherwise the shell treats it as a pipe.

Check what would run without running it:

npx playwright test --grep @smoke --list

⚙️ Filters in the Config and npm Scripts

Make common filters permanent:

// playwright.config.ts
export default defineConfig({
  grepInvert: /@flaky/,      // never run @flaky tests by default
  projects: [
    { name: 'smoke',      grep: /@smoke/,  use: { ...devices['Desktop Chrome'] } },
    { name: 'regression', grep: /@regression/, use: { ...devices['Desktop Chrome'] } },
  ],
});
{
  "scripts": {
    "test:smoke": "playwright test --grep @smoke",
    "test:regression": "playwright test --grep @regression --grep-invert @flaky",
    "test:api": "playwright test --grep @api"
  }
}

Then npm run test:smoke or npx playwright test --project=smoke. More command options are in Playwright CLI commands.


🧵 Control How a Group Runs: describe.configure

// Run tests in this file in parallel, even if fullyParallel is false
test.describe.configure({ mode: 'parallel' });

// Run tests in order; if one fails, the rest are skipped
test.describe.serial('Checkout wizard', () => {
  test('step 1: shipping', async ({ page }) => { /* ... */ });
  test('step 2: payment', async ({ page }) => { /* ... */ });
});

// Retries and timeout for one group
test.describe('Reports', () => {
  test.describe.configure({ retries: 2, timeout: 60_000 });
  test('export PDF', async ({ page }) => { /* ... */ });
});

⚠️ Common mistake: Serial mode makes tests depend on each other, which slows the suite and spreads one failure to many tests. Prefer independent tests; use serial only for genuinely step-by-step flows.


🎯 Focus and Skip Groups While Developing

test.describe.only('Cart', () => { /* only this group runs */ });
test.describe.skip('Wishlist', () => { /* this group is skipped */ });
test.only('debug this one test', async ({ page }) => { /* ... */ });

Never commit .only: set forbidOnly: !!process.env.CI in the config so CI fails if one slips through. Skipping with conditions, fixme and slow are covered in annotations.


🔎 Read Tags at Runtime

test('tagged test', { tag: '@smoke' }, async ({ page }, testInfo) => {
  console.log(testInfo.tags); // ['@smoke']
});

This is useful in reporters, hooks and integrations with test management tools.


📝 Assignment

  1. Organize your Sauce Demo tests into describe blocks: Login, Cart (with nested Add items and Remove items) and Sorting.
  2. Tag two critical tests @smoke, all tests @regression, and one test @slow.
  3. Run only @smoke tests, then everything except @slow, and use --list to confirm each selection first.
  4. Run tests that are both @regression and @smoke using a lookahead pattern.
  5. Add test:smoke and test:regression scripts to package.json.
  6. Create a smoke project in the config with grep: /@smoke/ and run it with --project=smoke.
  7. Bonus: print testInfo.tags in an afterEach hook for every test.

❓ FAQ

🤔 How do I tag tests in Playwright?

Pass a tag option: test('title', { tag: '@smoke' }, async ({ page }) => {}), or add @smoke to the test title. Tags can also be applied to a whole test.describe block.

🤔 How do I run only smoke tests in Playwright?

Run npx playwright test --grep @smoke, or create a project with grep: /@smoke/ and run it with --project.

🤔 How do I exclude tests in Playwright?

Use --grep-invert @tag on the command line, or grepInvert in the config.

🤔 What does test.describe do in Playwright?

It groups tests under a shared title, scopes hooks and configuration to that group, and makes reports easier to read.

🤔 How do I run Playwright tests in a specific order?

Use test.describe.serial or test.describe.configure({ mode: 'serial' }). Tests then run one after another in declaration order, and the rest are skipped if one fails.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Hooks: beforeEach, afterEach, beforeAll and afterAll Explained

Hooks let you run setup and cleanup code automatically around your tests, so every test starts from a known state without repeating code. This lesson explains all four Playwright hooks, their scope and order, common pitfalls, and when fixtures are the better choice.

📖 What You’ll Learn: beforeEach and afterEach, acting on failures with testInfo, beforeAll and afterAll with worker fixtures, hook scope inside describe blocks, execution order, titled hooks, hooks vs fixtures, and best practices.


🪝 What Are Hooks?

Hooks are functions that run automatically before or after your tests. They remove repetition: instead of writing the same login steps at the start of ten tests, you write them once in a hook.

Playwright Test has four hooks:

HookRunsTypical use
test.beforeEachBefore every test in its scopeOpen the page, log in
test.afterEachAfter every test in its scopeLog results, clean up data, extra screenshots
test.beforeAllOnce, before all tests in its scope (per worker)Create test data via API, start a service
test.afterAllOnce, after all tests in its scope (per worker)Delete test data, close connections

🔁 beforeEach and afterEach

The most common hook. Here every test starts on the logged-in products page:

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

test.beforeEach(async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page.locator('.title')).toHaveText('Products');
});

test.afterEach(async ({ page }, testInfo) => {
  console.log(testInfo.title + ' finished with status: ' + testInfo.status);
});

test('add backpack to cart', async ({ page }) => {
  await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
  await expect(page.locator('.shopping_cart_badge')).toHaveText('1');
});

test('sort products by price', async ({ page }) => {
  await page.locator('[data-test="product-sort-container"]').selectOption('lohi');
  await expect(page.locator('.inventory_item_price').first()).toHaveText('$7.99');
});

Hooks receive the same fixtures as tests (page, context, request…). The second argument, testInfo, describes the current test: its title, status, retry number and more.

Do something only when a test fails

test.afterEach(async ({ page }, testInfo) => {
  if (testInfo.status !== testInfo.expectedStatus) {
    const image = await page.screenshot({ fullPage: true });
    await testInfo.attach('failure-full-page', { body: image, contentType: 'image/png' });
    console.log('Failed on URL: ' + page.url());
  }
});

🧱 beforeAll and afterAll

beforeAll runs once before the tests in its scope. It is ideal for expensive setup that tests can share, such as creating data through an API:

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

let postId: number;

test.beforeAll(async ({ request }) => {
  const response = await request.post('https://jsonplaceholder.typicode.com/posts', {
    data: { title: 'Playwright hooks', body: 'demo', userId: 1 },
  });
  expect(response.ok()).toBeTruthy();
  postId = (await response.json()).id;
  console.log('Created post ' + postId);
});

test.afterAll(async ({ request }) => {
  await request.delete('https://jsonplaceholder.typicode.com/posts/' + postId);
  console.log('Deleted post ' + postId);
});

test('post id was created', async () => {
  expect(postId).toBeGreaterThan(0);
});

⚠️ Common mistake: beforeAll and afterAll can only use worker-scoped fixtures such as browser and request. The page fixture belongs to a single test and is not available in beforeAll. If you need a page there, create one: const page = await browser.newPage(), and close it in afterAll.

📝 Note: “Once” means once per worker process. With four parallel workers, a top-level beforeAll in a file can run up to four times, and it runs again after a failed test, because Playwright restarts the worker. For setup that must run exactly once for the whole run, use a setup project or globalSetup (config file).


📦 Hook Scope with test.describe

Hooks declared at the top of a file apply to every test in the file. Hooks inside a test.describe block apply only to the tests in that block:

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

test.beforeEach(async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');            // runs for ALL tests in the file
});

test.describe('valid login', () => {
  test.beforeEach(async ({ page }) => {                      // runs only in this block
    await page.getByPlaceholder('Username').fill('standard_user');
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
  });

  test('shows products', async ({ page }) => {
    await expect(page.locator('.title')).toHaveText('Products');
  });
});

test.describe('invalid login', () => {
  test('locked out user sees an error', async ({ page }) => {
    await page.getByPlaceholder('Username').fill('locked_out_user');
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page.locator('[data-test="error"]')).toContainText('locked out');
  });
});

🔢 Execution Order

For a test inside a describe block, Playwright runs:

  1. File-level beforeAll (once per worker)
  2. Describe-level beforeAll (once per worker)
  3. File-level beforeEach
  4. Describe-level beforeEach
  5. The test
  6. Describe-level afterEach
  7. File-level afterEach
  8. Describe-level afterAll (after the last test in the block)
  9. File-level afterAll (after the last test in the file)

Before hooks run from the outside in; after hooks run from the inside out.

// Hooks can have titles, which appear in the report and traces
test.beforeEach('open the login page', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
});

⚖️ Hooks vs Fixtures

Both remove repetition, but they work differently:

HooksFixtures
Reuse across filesCopy or import per fileDefined once, used anywhere
Setup and teardown togetherSplit into before and afterIn one function around use()
RunsFor every test in scopeOnly for tests that request it
Passes data to testsThrough shared variablesDirectly as a parameter
Best forSimple, file-specific setupReusable setup: logged-in pages, page objects, test data

Start with hooks while learning. As your suite grows, move shared setup into custom fixtures.


✅ Best Practices

  • Keep hooks short; tests should still be readable without scrolling up.
  • Do not share mutable state between tests through beforeAll variables unless tests run in serial mode; each test should work alone.
  • Put assertions in hooks only to confirm setup succeeded (for example, login reached the products page).
  • Use afterEach cleanup for data your test creates, so reruns start fresh.
  • Prefer a setup project with storageState over logging in through the UI in every beforeEach for large suites (save login session).

📝 Assignment

  1. Create tests/hooks.spec.ts with a beforeEach that logs in to Sauce Demo, and three tests: add to cart, sort by price, open the cart.
  2. Add an afterEach that prints the test title and status, and attaches a full-page screenshot only when a test fails.
  3. Add beforeAll and afterAll hooks that print a message; run with --workers=1 and then --workers=3 and explain the difference in output.
  4. Wrap two tests in a test.describe with their own beforeEach, and write down the execution order you observe.
  5. Use beforeAll with the request fixture to create a post on jsonplaceholder.typicode.com and assert its id in a test.
  6. Bonus: try to use page inside beforeAll, read the error, then fix it with browser.newPage().

❓ FAQ

🤔 What is the difference between beforeEach and beforeAll in Playwright?

beforeEach runs before every test and can use the page fixture. beforeAll runs once per worker before all tests in its scope and can only use worker-scoped fixtures such as browser and request.

🤔 Why can’t I use page in beforeAll?

The page fixture is created fresh for each test, so it does not exist yet in beforeAll. Create a page manually with browser.newPage() if you need one.

🤔 Why does my beforeAll run more than once?

beforeAll runs once per worker. With several workers, or after a test failure that restarts the worker, it runs again. Use globalSetup or a setup project for one-time setup.

🤔 Should I use hooks or fixtures in Playwright?

Hooks are fine for simple setup within a file. Fixtures are better for reusable setup across files, because they combine setup and teardown and only run for tests that use them.

🤔 How do I know if a test failed inside afterEach?

Compare testInfo.status with testInfo.expectedStatus; if they differ, the test failed unexpectedly.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Visual Regression Testing in Playwright with toHaveScreenshot

Functional tests miss broken layouts, missing icons and CSS mistakes. Playwright catches them with built-in visual comparisons. In this lesson you will create baselines, compare pages and components, tune tolerances, mask dynamic content and keep results consistent in CI.

📖 What You’ll Learn: what visual regression testing is, toHaveScreenshot for pages and elements, how baselines are stored and updated, maxDiffPixels and threshold, masking and animations, stylePath, cross-platform consistency, and text and ARIA snapshots.


👀 What Is Visual Regression Testing?

Functional tests check that things work: the button submits, the total is correct. They do not notice that the button is now white on white, the logo moved, or a CSS change broke the layout. Visual regression testing catches those problems by comparing screenshots of your application with approved baseline images, pixel by pixel.

Playwright has visual testing built in with toHaveScreenshot(). No extra library or paid service is needed.


🖼️ Your First Visual Test

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

test('login page looks correct', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await expect(page).toHaveScreenshot('login-page.png');
});

What happens on each run

  1. First run: there is no baseline yet, so the test fails with a message that the snapshot does not exist, and Playwright writes the screenshot as the new baseline.
  2. Second run: Playwright takes a new screenshot and compares it with the baseline. If they match, the test passes.
  3. When the UI changes: the test fails and the HTML report shows Expected, Actual and Diff images, with a slider to compare them.

Baselines are saved next to the test file:

tests/
├── visual.spec.ts
└── visual.spec.ts-snapshots/
    ├── login-page-chromium-win32.png
    ├── login-page-firefox-win32.png
    └── login-page-webkit-win32.png

The file name includes the project (browser) and the platform, because fonts and rendering differ between browsers and operating systems.

💡 Tip: Commit the -snapshots folders to Git. Baselines are part of your test suite, and reviewers can see image changes in pull requests.


🔄 Updating Baselines After an Intended Change

When the design changes on purpose, regenerate the baselines:

npx playwright test --update-snapshots
npx playwright test visual.spec.ts --update-snapshots   # only one file

Review the new images before committing them: an update accepts whatever the page looks like now, including bugs.


🧩 Screenshot a Single Component

Comparing the whole page makes tests sensitive to any change anywhere. Comparing one element keeps visual tests focused:

test('login form component', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await expect(page.locator('.login-box')).toHaveScreenshot('login-form.png');
});

test('full page including content below the fold', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await expect(page).toHaveScreenshot('login-full.png', { fullPage: true });
});

🎚️ Tolerances: Avoid False Failures

Tiny rendering differences (anti-aliasing, font smoothing) can produce a few different pixels. Control how strict the comparison is:

OptionMeaningExample
maxDiffPixelsAllowed number of different pixels100
maxDiffPixelRatioAllowed share of different pixels (0 to 1)0.01 (1 percent)
thresholdHow different a pixel’s colour must be to count (0 to 1, default 0.2)0.3
await expect(page).toHaveScreenshot('dashboard.png', { maxDiffPixelRatio: 0.01 });

Set defaults for every visual assertion in playwright.config.ts:

export default defineConfig({
  expect: {
    toHaveScreenshot: { maxDiffPixels: 50 },
  },
});

🎭 Handle Dynamic Content: Mask, Animations and Styles

Dates, ads, avatars, carousels and blinking cursors change on every run. Hide them so they do not cause failures:

await expect(page).toHaveScreenshot('home.png', {
  mask: [page.locator('.current-date'), page.locator('.ad-banner')], // covered with a solid box
  animations: 'disabled',  // finishes or stops CSS animations (default for toHaveScreenshot)
  caret: 'hide',           // hides the text cursor (default)
});

You can also inject CSS only for screenshots, for example to hide a chat widget:

await expect(page).toHaveScreenshot('home.png', {
  stylePath: 'tests/screenshot.css', // e.g. .chat-widget { visibility: hidden; }
});

📝 Note: toHaveScreenshot waits until two consecutive screenshots are identical before comparing, so pages that are still loading are handled automatically. Very dynamic pages still need masking.


🖥️ Consistent Results Locally and in CI

The most common visual testing problem: baselines created on Windows fail on a Linux CI machine because fonts render differently. Solutions:

  • Create baselines in the same environment as CI, for example in the official Playwright Docker image (mcr.microsoft.com/playwright). See Playwright with Docker.
  • Keep platform-specific baselines (the default naming already separates them by OS).
  • Run visual tests in one browser project (usually Chromium) to reduce the number of baselines.
  • Fix the viewport size in the config so layouts are identical.
// Run visual tests only in Chromium with a fixed viewport
projects: [
  { name: 'visual', testMatch: /.*visual\.spec\.ts/, use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 720 } } },
  { name: 'chromium', testIgnore: /.*visual\.spec\.ts/, use: { ...devices['Desktop Chrome'] } },
],

📄 Beyond Images: Text and ARIA Snapshots

  • expect(value).toMatchSnapshot('name.txt') compares text or binary data with a stored snapshot.
  • expect(locator).toMatchAriaSnapshot(...) compares the accessibility tree (roles and names) instead of pixels. It is less fragile than images for checking page structure.

✅ Visual Testing Best Practices

  • Start small: key pages and shared components (header, login form, checkout summary).
  • Prefer element screenshots over full pages.
  • Mask anything dynamic; never accept flaky baselines.
  • Review every --update-snapshots change like code.
  • Keep visual tests in a separate file or project so they can run on their own.

📝 Assignment

  1. Write a visual test for the Sauce Demo login page, run it twice, and find the baseline image in the -snapshots folder.
  2. Add an element visual test for .login-box.
  3. Break the baseline on purpose: run once with await page.setViewportSize({ width: 800, height: 600 }) before the screenshot, then open the HTML report and study the Expected, Actual and Diff views.
  4. Add maxDiffPixels: 50 as a default in the config.
  5. Mask the .login_credentials_wrap section and regenerate the baseline with --update-snapshots.
  6. Bonus: create a visual project that runs only *visual.spec.ts files in Chromium with a fixed viewport.

❓ FAQ

🤔 How do I do visual testing in Playwright?

Use await expect(page).toHaveScreenshot('name.png') or await expect(locator).toHaveScreenshot(). The first run creates the baseline; later runs compare against it.

🤔 Why does my visual test fail on the first run?

There is no baseline yet. Playwright writes the current screenshot as the baseline and reports the missing snapshot. Run the test again and it will compare against the new baseline.

🤔 How do I update Playwright screenshots?

Run npx playwright test --update-snapshots (or -u), review the new images, and commit them.

🤔 What is the difference between page.screenshot and toHaveScreenshot?

page.screenshot() just saves an image. toHaveScreenshot() is an assertion that compares the page with a stored baseline and fails if they differ. See screenshots and videos.

🤔 Why do visual tests pass locally but fail in CI?

Different operating systems and fonts render pixels differently. Generate baselines in the same environment as CI (for example the Playwright Docker image) and use small tolerances.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Screenshots and Video Recording: Complete Guide

Screenshots and videos turn a confusing test failure into an obvious one. In this lesson you will capture full-page and element screenshots, mask sensitive data, attach images to the HTML report, and configure automatic screenshots and videos for failed tests.

📖 What You’ll Learn: page and element screenshots, screenshot options (fullPage, clip, mask, animations), attaching images with testInfo.attach, automatic screenshots and videos in the config, video size, recording in plain scripts, and screenshots vs videos vs traces.


📸 Why Screenshots and Videos Matter

When a test fails in CI at 2 a.m., the error message alone rarely tells the whole story. A screenshot shows what the page looked like at the moment of failure, and a video shows every step leading up to it. Playwright can capture both automatically, with one line in the config.


🖼️ Take a Screenshot of the Page

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

test('capture the login page', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');

  await page.screenshot({ path: 'screenshots/login.png' });                  // visible area
  await page.screenshot({ path: 'screenshots/login-full.png', fullPage: true }); // whole scrollable page
});

The folder is created automatically. Paths are relative to where you run the command (usually the project root).

Useful screenshot options

OptionWhat it does
pathWhere to save the file (.png or .jpeg)
fullPage: trueCapture the entire scrollable page
type: 'jpeg' with quality: 80Smaller files
clip: { x, y, width, height }Capture only an area of the page
mask: [locator]Cover sensitive or changing elements with a box
animations: 'disabled'Stop CSS animations for a stable image
omitBackground: trueTransparent background (PNG only)
await page.screenshot({
  path: 'screenshots/masked.png',
  fullPage: true,
  mask: [page.locator('[data-test="username"]'), page.locator('[data-test="password"]')],
  animations: 'disabled',
});

🎯 Screenshot a Single Element

Call screenshot() on a locator to capture just that element:

test('capture the login form only', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.locator('.login-box').screenshot({ path: 'screenshots/login-form.png' });
});

Element screenshots are ideal for bug reports and for visual regression tests of components.


📎 Attach Screenshots to the HTML Report

Saving files to a folder is fine locally, but in a team you want screenshots inside the report. Use testInfo.attach():

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

test('cart badge after adding a product', async ({ page }, testInfo) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();

  const image = await page.screenshot();               // returns a Buffer when no path is given
  await testInfo.attach('cart-after-add', { body: image, contentType: 'image/png' });

  await expect(page.locator('.shopping_cart_badge')).toHaveText('1');
});

Open the HTML report (npx playwright show-report) and the image appears under Attachments for that test.


⚙️ Automatic Screenshots on Failure (Config)

You do not need to write screenshot code in every test. Set it once in playwright.config.ts:

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

export default defineConfig({
  use: {
    screenshot: 'only-on-failure', // 'off' | 'on' | 'only-on-failure'
  },
});
ValueBehaviourGood for
'off'No automatic screenshots (default)Fast local runs
'on'Screenshot after every testAudits, demos
'only-on-failure'Screenshot only when a test failsRecommended for most projects

Screenshots are saved in test-results/ and embedded in the HTML report.


🎬 Record Videos of Test Runs

Videos are also configured with one option:

use: {
  video: 'retain-on-failure', // 'off' | 'on' | 'retain-on-failure' | 'on-first-retry'
},
ValueBehaviour
'off'No video (default)
'on'Record every test
'retain-on-failure'Record every test, keep only failures
'on-first-retry'Record only when a failed test is retried

To control the resolution:

use: {
  video: { mode: 'retain-on-failure', size: { width: 1280, height: 720 } },
},

Videos are saved as .webm files in test-results/ and can be played directly in the HTML report.

📝 Note: A video is finished only when the browser context closes, which Playwright does at the end of each test. If you need the file path inside a test, use await page.video()?.path().

Recording video outside the test runner

When you use the Playwright library in a plain script, enable recording on the context:

import { chromium } from 'playwright';

(async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext({ recordVideo: { dir: 'videos/' } });
  const page = await context.newPage();
  await page.goto('https://www.saucedemo.com/');
  await context.close(); // the video is written now
  await browser.close();
})();

🧭 Screenshots, Videos or Traces?

ArtifactShowsCost
ScreenshotOne moment, usually the failureVery small
VideoEvery step as a recordingLarger files, slight slowdown
TraceEvery action, DOM snapshot, network call and console log, step by stepMedium; the most useful for debugging

A balanced setup for most teams:

use: {
  screenshot: 'only-on-failure',
  video: 'retain-on-failure',
  trace: 'on-first-retry',
},

Traces are the most powerful debugging tool of the three. They get their own lesson, Trace Viewer step by step, in Module 11.


✅ Best Practices

  • Use 'only-on-failure' and 'retain-on-failure' to keep CI artifacts small.
  • Mask passwords, personal data and changing content (dates, ads) in screenshots.
  • Do not commit test-results/ or screenshot folders to Git; the default .gitignore already excludes them.
  • In CI, upload playwright-report/ as an artifact so the team can open screenshots and videos (covered in the GitHub Actions lesson).
  • Attach screenshots with testInfo.attach() when a step is important, so they appear in the report next to the test.

📝 Assignment

  1. Take a full-page screenshot of https://www.saucedemo.com and an element screenshot of the login form.
  2. Take a screenshot with the username and password fields masked, and compare it with an unmasked one.
  3. Log in, add two products, and attach a screenshot of the cart page to the HTML report with testInfo.attach().
  4. Configure screenshot: 'only-on-failure' and video: 'retain-on-failure'. Make one test fail on purpose and find both artifacts in the HTML report.
  5. Set the video size to 1280 x 720 and confirm the new resolution.
  6. Bonus: write a plain Node script (no test runner) that records a video of opening three different pages.

❓ FAQ

🤔 How do I take a screenshot in Playwright?

await page.screenshot({ path: 'file.png' }) for the page, add fullPage: true for the whole page, or call locator.screenshot() for one element.

🤔 How do I take a screenshot only when a test fails?

Set use: { screenshot: 'only-on-failure' } in playwright.config.ts. The screenshot is saved in test-results/ and shown in the HTML report.

🤔 How do I record a video in Playwright?

Set use: { video: 'on' } (or 'retain-on-failure') in the config. In a plain script, create the context with recordVideo: { dir: 'videos/' }.

🤔 Where are Playwright screenshots and videos saved?

Automatic ones go to the test-results/ folder (configurable with outputDir) and are linked in the HTML report. Manual screenshots go to the path you provide.

🤔 How do I hide sensitive data in a screenshot?

Pass locators to the mask option: page.screenshot({ mask: [page.locator('#password')] }).


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Codegen: Record Tests, Pick Locators and Generate Code

Playwright Codegen records your clicks and typing and turns them into test code. In this lesson you will use it from the terminal and VS Code, add assertions while recording, record logged-in flows, and refactor the generated code into a clean, reliable test.

📖 What You’ll Learn: starting codegen, the Inspector toolbar, assertions while recording, options for devices, browsers and test ids, saving and loading sessions, recording in VS Code, page.pause(), and a refactoring checklist for recorded code.


🎥 What Is Playwright Codegen?

Codegen is Playwright’s built-in test recorder. You use the website normally in a browser window, and Playwright writes the test code for every click, fill and navigation in real time. It also helps you:

  • Find the best locator for any element (“Pick locator”).
  • Add assertions by clicking elements (visible, text, value).
  • Learn the Playwright API quickly by watching what code each action produces.

It is a learning and productivity tool, not a replacement for designing tests. You will record, then refactor the code into clean tests.


▶️ Start Codegen from the Terminal

From your Playwright project folder (see Install Playwright):

npx playwright codegen https://www.saucedemo.com

Two windows open:

  1. A browser window where you interact with the site.
  2. The Playwright Inspector, which shows the generated code and a toolbar.

Now log in with standard_user / secret_sauce and add a product to the cart. The Inspector writes code like this:

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

test('test', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.locator('[data-test="username"]').click();
  await page.locator('[data-test="username"]').fill('standard_user');
  await page.locator('[data-test="password"]').click();
  await page.locator('[data-test="password"]').fill('secret_sauce');
  await page.locator('[data-test="login-button"]').click();
  await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
});

The exact locators depend on your Playwright version and the page, but Codegen always prefers stable, user-facing locators: roles, labels, placeholders, text and test ids.


🧰 The Inspector Toolbar

ButtonWhat it does
RecordStart or stop recording actions
Pick locatorHover and click any element to get its locator
Assert visibilityClick an element to add toBeVisible()
Assert textClick an element to add toContainText()
Assert valueClick an input to add toHaveValue()
CopyCopy the generated code
ClearStart a fresh recording
Target dropdownChoose the output language (Playwright Test TypeScript, Python, Java, C#…)

💡 Tip: Use Assert text and Assert visibility while recording. A recorded test without assertions only proves that clicks did not crash; it does not prove that the application worked.


⚙️ Useful Codegen Options

# Save the code straight to a file
npx playwright codegen -o tests/recorded.spec.ts https://www.saucedemo.com

# Emulate a mobile device
npx playwright codegen --device="iPhone 13" https://www.saucedemo.com

# Choose the browser: cr (Chromium), ff (Firefox), wk (WebKit)
npx playwright codegen -b wk https://www.saucedemo.com

# Set the window size, color scheme, language or time zone
npx playwright codegen --viewport-size="1280,720" --color-scheme=dark --lang="en-IN" --timezone="Asia/Kolkata" https://example.com

# Use your own test id attribute in generated locators
npx playwright codegen --test-id-attribute=data-test https://www.saucedemo.com

Record once logged in

Save the session after you log in, then start later recordings already authenticated:

# 1. Log in manually, then close the browser: cookies and storage are saved
npx playwright codegen --save-storage=auth.json https://www.saucedemo.com

# 2. Next recordings start logged in
npx playwright codegen --load-storage=auth.json https://www.saucedemo.com/inventory.html

The same storageState idea is used in real test suites: see save and reuse login sessions.

⚠️ Common mistake: Files like auth.json contain real session cookies. Add them to .gitignore and never commit them.


🧑‍💻 Record from VS Code

With the Playwright Test for VSCode extension:

  • Record new: creates a new test file and opens a browser; actions are written directly into the file.
  • Record at cursor: inserts recorded actions where your cursor is in an existing test.
  • Pick locator: click an element in the browser and the locator is copied into the editor’s locator box.

This is often faster than the terminal, because code lands straight in your project.


⏸️ Record in the Middle of a Test with page.pause()

Add await page.pause() to any test and run it headed. The test stops there and opens the Inspector, where you can record new steps from the current state of the page:

test('extend an existing flow', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();

  await page.pause(); // Inspector opens here; click Record to capture the next steps
});
npx playwright test tests/extend.spec.ts --headed

Remove page.pause() before committing the test.


🧹 From Recording to a Real Test: Refactor Checklist

Recorded code is a draft. Turn it into a maintainable test:

  1. Give the test a real name instead of 'test'.
  2. Remove redundant steps such as a click() right before a fill() on the same field.
  3. Add assertions for the outcome of each important step.
  4. Use baseURL and page.goto('/') instead of the full URL (config file).
  5. Move test data out of the code (credentials, product names) into variables or files.
  6. Replace fragile locators like nth(3) with role or text locators (locators, CSS and XPath).
  7. Extract repeated flows (login) into helpers or page objects.

The recorded test above, refactored:

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

const user = { username: 'standard_user', password: 'secret_sauce' };

test('standard user can add a backpack to the cart', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill(user.username);
  await page.getByPlaceholder('Password').fill(user.password);
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page.locator('.title')).toHaveText('Products');

  await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
  await expect(page.locator('.shopping_cart_badge')).toHaveText('1');
});

⚖️ Codegen: Strengths and Limits

Good forNot good for
Learning the API and locatorsDesigning test structure and coverage
Quickly drafting a happy pathData-driven and negative scenarios
Finding locators on complex pagesReusable code (no page objects or fixtures)
Reproducing a bug reportComplex logic, loops and conditions

📝 Assignment

  1. Run npx playwright codegen https://www.saucedemo.com and record: log in, add two products, open the cart. Save the code to tests/recorded-cart.spec.ts with -o.
  2. While recording, add at least two assertions using Assert text and Assert visibility.
  3. Refactor the recording using the seven-step checklist above, and make it pass headless.
  4. Record the same flow with --device="Pixel 5" and compare any differences in the generated code.
  5. Use --save-storage to save a logged-in session, then start a new recording with --load-storage directly on the inventory page.
  6. Bonus: add page.pause() to your test, record one more step from the Inspector, then remove pause().

❓ FAQ

🤔 What is Playwright codegen?

A built-in recorder that generates Playwright test code while you interact with a website. Start it with npx playwright codegen <url>.

🤔 How do I record a test in Playwright?

Run npx playwright codegen https://your-site.com, perform the actions in the browser, and copy the generated code from the Inspector, or use Record new in the VS Code extension.

🤔 Can Playwright codegen generate TypeScript?

Yes. The default target for Playwright Test is TypeScript/JavaScript, and you can switch to Python, Java or C# in the Inspector’s target dropdown.

🤔 Is codegen code good enough for production tests?

Treat it as a first draft. Rename tests, add assertions, remove redundant steps, move test data out and use page objects before adding it to your suite.

🤔 How do I get a locator for one element without recording?

Use Pick locator in the Inspector or the VS Code extension, then hover and click the element.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in