Playwright Test Agents: Planner, Generator and Healer Explained

Playwright now ships with AI agents that plan, generate and heal tests. In this lesson you will set them up, write a seed test, turn a Markdown test plan into Playwright tests, and use the healer safely when the UI changes.

📖 What You’ll Learn: what the planner, generator and healer agents do, init-agents for VS Code, Claude, Codex and OpenCode, the seed test, reviewing Markdown test plans, generating tests, healing failures, your role at each step, and best practices.


🤝 What Are Playwright Test Agents?

Playwright Test Agents are three ready-made AI agents, shipped with Playwright, that help you plan, write and maintain end-to-end tests:

AgentInputOutput
🎭 PlannerYour app (plus a seed test and optional requirements)A Markdown test plan in specs/
🎭 GeneratorThe Markdown planPlaywright Test files in tests/, with locators and assertions verified live in the browser
🎭 HealerA failing testA fixed test (locators, waits, data), or a skipped test if it believes the feature is really broken

They run inside AI coding tools (VS Code with Copilot, Claude Code, Codex, OpenCode) and use Playwright’s browser tools behind the scenes, similar to Playwright MCP. You can use them one at a time, in sequence, or as a loop.


⚙️ Set Up the Agents

Start from a working Playwright TypeScript project (installation) on a recent Playwright version, then generate the agent definitions for your AI tool:

npx playwright init-agents --loop=vscode     # VS Code (GitHub Copilot agent mode)
npx playwright init-agents --loop=claude     # Claude Code
npx playwright init-agents --loop=codex      # Codex
npx playwright init-agents --loop=opencode   # OpenCode

📝 Note: For VS Code, the agentic experience needs VS Code 1.105 or newer. Re-run init-agents whenever you update Playwright, so the agent definitions match the new version.

After initialization your project looks like this:

.github/                     # agent definitions (location depends on the AI tool)
specs/                       # human-readable Markdown test plans
tests/
  └── seed.spec.ts           # seed test: sets up the environment for the agents
playwright.config.ts

🌱 The Seed Test

The seed test prepares everything the agents need to start: navigation to the app, login, fixtures and global setup. The planner runs it first, and generated tests copy its setup. Keep it small:

// tests/seed.spec.ts
import { test, expect } from '@playwright/test';

test('seed', 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');
});

If your project uses custom fixtures or a logged-in storage state, use them in the seed test so generated tests follow the same pattern.


🗺️ Step 1: The Planner Writes a Test Plan

Open your AI tool’s chat, choose the planner agent and describe what to cover:

💬 “Using the seed test, create a test plan for the cart and checkout flows of Sauce Demo: adding and removing items, cart badge, checkout information validation and order completion.”

The planner explores the app in a real browser and saves a plan such as specs/cart-and-checkout.md:

# Cart and Checkout Test Plan

## Application Overview
Sauce Demo lets a logged-in user add products to a cart, review the cart,
enter checkout information and complete an order.

## Test Scenarios

### 1. Cart
**Seed:** tests/seed.spec.ts

#### 1.1 Add a single product
**Steps:**
1. Click "Add to cart" for Sauce Labs Backpack
**Expected Results:**
- The cart badge shows 1
- The button changes to "Remove"

#### 1.2 Remove a product from the cart page
...

### 2. Checkout
#### 2.1 Postal code is required
...

💡 Tip: Review and edit the plan before generating tests. This is where your testing knowledge matters most: add negative cases, remove duplicates, and make the expected results precise. A Product Requirements Document (PRD) given to the planner improves the plan.


🛠️ Step 2: The Generator Writes the Tests

Choose the generator agent and point it at the plan:

💬 “Generate tests for section 1 of specs/cart-and-checkout.md.”

The generator walks through each scenario in the browser, checks that locators and assertions really work, and writes files into tests/, for example:

// tests/cart/add-single-product.spec.ts
// spec: specs/cart-and-checkout.md
// seed: tests/seed.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Cart', () => {
  test('Add a single product', 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.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
    await expect(page.locator('.shopping_cart_badge')).toHaveText('1');
    await expect(page.locator('[data-test="remove-sauce-labs-backpack"]')).toBeVisible();
  });
});

Generated code differs between runs and versions; always read it, run it, and refactor it into your framework structure (page objects, fixtures, data files).


🩹 Step 3: The Healer Fixes Failing Tests

When the UI changes and tests fail, choose the healer agent:

💬 “Run the cart tests and fix any failures.”

The healer:

  1. Runs the failing test.
  2. Inspects the current UI to find equivalent elements or flows.
  3. Patches the test: an updated locator, a missing wait condition, adjusted test data.
  4. Re-runs it until it passes, or marks the test as skipped if it concludes that the application itself is broken.

⚠️ Common mistake: A healer that “fixes” a test can also hide a real bug, for example by changing an expected value to match a wrong result. Review every healed diff in a pull request, exactly as you would review a colleague’s change.


🔁 Putting It Together

StepAgentYour role
1. PlanPlannerProvide seed test and requirements, review the plan
2. GenerateGeneratorReview code, refactor into page objects, add to CI
3. MaintainHealerReview healed changes; investigate skipped tests as possible bugs

Run the resulting tests like any other suite: locally, in GitHub Actions or Jenkins. The agents speed up writing and maintenance; the suite itself stays plain, deterministic Playwright code.


✅ Best Practices

  • Start small: one feature, one plan, a handful of tests.
  • Keep the seed test fast and reliable; every generated test depends on it.
  • Use test environments and test accounts only.
  • Store plans (specs/) in Git next to the tests; they document coverage for the team.
  • Require code review for generated and healed tests.
  • Combine with tags so agent-generated tests are easy to find and run.

📝 Assignment

  1. In your Sauce Demo project, run npx playwright init-agents for the AI tool you use.
  2. Write a seed test that logs in and asserts the products page.
  3. Ask the planner for a test plan covering sorting and the cart. Review it and add two negative scenarios yourself.
  4. Ask the generator to create tests for the sorting section. Run them and fix anything that fails.
  5. Change one locator in a generated test to something wrong, then ask the healer to fix it. Review its diff.
  6. Bonus: refactor the generated tests to use your page objects and fixtures from Project 2.

❓ FAQ

🤔 What are Playwright Test Agents?

Three AI agents included with Playwright: a planner that writes Markdown test plans, a generator that turns plans into Playwright tests, and a healer that repairs failing tests.

🤔 How do I set up Playwright Test Agents?

Run npx playwright init-agents --loop=vscode (or claude, codex, opencode) in your Playwright project, add a seed test, then use the agents from your AI tool’s chat.

🤔 What is the seed test in Playwright agents?

A test that sets up the environment (navigation, login, fixtures). The planner runs it first and generated tests reuse its setup.

🤔 Can the healer agent hide real bugs?

It can if you accept changes blindly. The healer skips tests it believes reflect broken functionality, but you should review every healed change.

🤔 What is the difference between Playwright Test Agents and Playwright MCP?

Playwright MCP is a general browser tool for any AI client. Test Agents are purpose-built agents for planning, generating and healing Playwright Test suites.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright MCP: Browser Automation with AI Agents (Setup and Examples)

Playwright MCP gives AI assistants a real browser they can drive with Playwright. In this lesson you will understand how it works, install it in popular AI clients, learn its tools and options, and use it for exploration, test drafting and debugging.

📖 What You’ll Learn: what MCP and Playwright MCP are, accessibility snapshots vs screenshots, installation in Claude Code, VS Code and Cursor, the browser tools, configuration options, practical tester workflows, safety practices, and MCP vs Test Agents.


🤖 What Is Playwright MCP?

MCP (Model Context Protocol) is an open standard that lets AI assistants use external tools. Playwright MCP (@playwright/mcp) is Microsoft’s MCP server that gives an AI assistant, such as Claude, GitHub Copilot in VS Code or Cursor, a real browser it can control through Playwright.

With it, you can ask an assistant in plain English:

  • “Open the Sauce Demo site, log in as standard_user and tell me what is on the products page.”
  • “Check whether the checkout form shows an error when the postcode is empty.”
  • “Explore this page and write a Playwright TypeScript test for adding two items to the cart.”

The assistant then navigates, clicks, types and reads the page step by step, using Playwright under the hood.

New to AI agents? Read Fundamentals of Agentic AI for Beginners first.


🧠 How It Works: Accessibility Snapshots, Not Screenshots

By default, Playwright MCP does not send screenshots to the AI. It sends an accessibility snapshot: a structured, text-based view of the page with roles, names and references for each element, the same information behind getByRole locators.

ApproachPlaywright MCP defaultScreenshot-based agents
Input to the AIStructured accessibility tree (text)Pixels
Speed and costFast, smallSlower, larger
PrecisionActs on exact elementsGuesses coordinates
Needs a vision modelNoYes

Screenshots are still available as a separate tool when the assistant needs to “see” the page.


⚙️ Install Playwright MCP

You need Node.js (see Install Playwright) and an MCP-capable AI client.

Standard configuration (most clients)

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}

Claude Code

claude mcp add playwright npx @playwright/mcp@latest

VS Code (GitHub Copilot agent mode)

code --add-mcp '{"name":"playwright","command":"npx","args":["@playwright/mcp@latest"]}'

Cursor

Open Cursor Settings, MCP, Add new MCP Server, choose the command type and enter npx @playwright/mcp@latest.

After installing, restart the client and check that the Playwright tools are listed and enabled.


🧰 The Tools the AI Gets

The server exposes browser actions as tools. The most important ones:

ToolWhat the AI can do
browser_navigate, browser_navigate_backOpen URLs, go back
browser_snapshotRead the page as an accessibility snapshot
browser_click, browser_hover, browser_dragMouse interactions
browser_type, browser_fill_form, browser_press_keyTyping and keyboard
browser_select_option, browser_file_uploadDropdowns and uploads
browser_handle_dialogAccept or dismiss alerts
browser_wait_forWait for text to appear or disappear
browser_tabsList, open, switch and close tabs
browser_take_screenshotCapture an image when visual context is needed
browser_console_messages, browser_network_requestsRead console logs and network traffic
browser_evaluateRun JavaScript on the page
browser_close, browser_resizeManage the browser window

🎛️ Useful Options

Add options to the args array in the configuration:

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--browser", "chrome", "--isolated", "--device", "iPhone 15"]
    }
  }
}
OptionPurpose
--headlessRun without a visible window (headed is the default)
--browserchrome, firefox, webkit or msedge
--isolatedKeep the browser profile in memory; nothing is saved between sessions
--user-data-dirUse a persistent profile folder
--storage-stateStart an isolated session with saved cookies and storage (login sessions)
--deviceEmulate a device such as "iPhone 15" (mobile emulation)
--capsEnable extra capabilities: vision, pdf, devtools
--output-dirFolder for screenshots and other output files

🧪 Practical Workflows for Testers

1. Exploratory testing assistant

💬 “Using the Playwright tools, open https://www.saucedemo.com, log in as standard_user with password secret_sauce, sort products by price high to low, and report the first three products and their prices.”

The assistant performs the steps and reports what it saw, which is useful for quick checks and reproducing bugs.

2. Generate a first draft of a test

💬 “Explore the checkout flow on Sauce Demo with the browser, then write a Playwright Test in TypeScript that adds the backpack, checks out with test data, and asserts the order confirmation. Use getByRole and data-test locators.”

Because the assistant has seen the real page, its locators are usually accurate. Treat the result like Codegen output: review it, add assertions and move it into your framework structure.

3. Debug a failing test

💬 “This test fails at the checkout button. Open the page, take a snapshot at that step, and explain why the button cannot be clicked.”

The assistant can read console errors and network requests to suggest causes.


⚠️ Safety and Good Practice

  • Use test environments and test accounts. The AI acts with whatever access the browser has.
  • Prefer --isolated so sessions do not reuse real cookies or logins.
  • Review every action your client asks to approve, especially form submissions.
  • Do not share secrets in prompts; use --storage-state with a test session instead.
  • Keep humans in charge of assertions. The AI can draft tests, but you decide what “correct” means.

🔄 Playwright MCP vs Playwright Test Agents

Playwright MCPPlaywright Test Agents
What it isAn MCP server giving any AI client browser controlReady-made planner, generator and healer agents for Playwright Test
Best forExploration, ad-hoc tasks, drafting tests, debuggingA structured loop: plan, generate and heal a test suite
OutputWhatever you ask forMarkdown test plans and Playwright test files

📝 Assignment

  1. Install Playwright MCP in your AI client (Claude Code, VS Code or Cursor) and confirm the tools are listed.
  2. Ask the assistant to open https://www.saucedemo.com and describe the login page using a snapshot.
  3. Ask it to log in, add two products to the cart and report the cart badge number.
  4. Ask it to write a Playwright TypeScript test for that flow, then run the test locally and fix anything that fails.
  5. Restart the server with --isolated --device "iPhone 15" and repeat task 2. What changed?
  6. Bonus: compare the AI-generated test with your own test from your first Playwright test. Which locators and assertions are better?

❓ FAQ

🤔 What is Playwright MCP?

An MCP server from Microsoft (@playwright/mcp) that lets AI assistants control a real browser through Playwright, using accessibility snapshots to understand the page.

🤔 How do I install Playwright MCP in Claude Code?

Run claude mcp add playwright npx @playwright/mcp@latest, then restart Claude Code.

🤔 Does Playwright MCP use screenshots?

Not by default. It uses accessibility snapshots, which are faster and more precise. A screenshot tool is available when visual context is needed.

🤔 Can Playwright MCP replace my test suite?

No. It is excellent for exploration, drafting and debugging, but a reliable regression suite still needs reviewed, deterministic Playwright tests running in CI.

🤔 Which browsers does Playwright MCP support?

Chrome, Firefox, WebKit and Microsoft Edge, selected with the --browser option.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Run Playwright Tests in Jenkins: Jenkinsfile, Docker, Reports and Credentials

Many companies run their pipelines on Jenkins. In this lesson you will run a Playwright TypeScript suite in a Jenkins pipeline using the official Docker image, publish JUnit results and the HTML report, handle credentials, add parameters and schedules, and shard large suites.

📖 What You’ll Learn: preparing the config for Jenkins, a Jenkinsfile with the official Docker image, agents without Docker and Windows agents, credentials, parameters and cron triggers, parallel shards, viewing the HTML report despite Jenkins CSP, and a Jenkins checklist.


🏗️ Why Jenkins?

Jenkins is the most widely used self-hosted CI server, especially in enterprises and service companies. If your team already builds and deploys with Jenkins, running Playwright there keeps everything in one pipeline. Playwright works well in Jenkins with a few Jenkins-specific settings you will learn here.

You need:

  • A Jenkins server (or a local one) with the Pipeline, Git, JUnit and HTML Publisher plugins.
  • Either Docker on the Jenkins agent (recommended) or Node.js installed on the agent.
  • Your Playwright project in a Git repository.

⚙️ Step 1: Prepare the Playwright Config for Jenkins

Jenkins does not set the CI environment variable that the default config checks, so you will set it in the pipeline. Also add a JUnit reporter so Jenkins can show test results natively (reporters):

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: [
    ['line'],
    ['html', { open: 'never' }],
    ['junit', { outputFile: 'results/junit.xml' }],
  ],
  use: {
    baseURL: process.env.BASE_URL ?? 'https://www.saucedemo.com',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
});

🐳 Step 2: Jenkinsfile with the Official Docker Image (Recommended)

Create a Jenkinsfile in the project root. The official Playwright image already contains Node.js, browsers and all system libraries:

pipeline {
  agent {
    docker {
      // Use the SAME version as @playwright/test in your package.json
      image 'mcr.microsoft.com/playwright:v1.xx.x-noble'
      args '--ipc=host'
    }
  }

  environment {
    CI = 'true'
    BASE_URL = 'https://www.saucedemo.com'
  }

  options {
    timeout(time: 60, unit: 'MINUTES')
  }

  stages {
    stage('Install') {
      steps {
        sh 'npm ci'
      }
    }
    stage('Test') {
      steps {
        sh 'npx playwright test'
      }
    }
  }

  post {
    always {
      junit allowEmptyResults: true, testResults: 'results/junit.xml'
      archiveArtifacts artifacts: 'playwright-report/**, test-results/**', allowEmptyArchive: true
      publishHTML(target: [
        allowMissing: true,
        alwaysLinkToLastBuild: true,
        keepAll: true,
        reportDir: 'playwright-report',
        reportFiles: 'index.html',
        reportName: 'Playwright Report'
      ])
    }
  }
}

What each part does:

SectionPurpose
agent { docker { ... } }Runs the build inside the official Playwright container
args '--ipc=host'Recommended for Chromium in Docker to avoid running out of shared memory
environmentSets CI so the config uses CI settings, plus non-secret variables
npm ciInstalls exact dependency versions from package-lock.json
junitShows passed and failed tests and trends in Jenkins
archiveArtifactsKeeps the HTML report, traces and screenshots for download
publishHTMLAdds a Playwright Report link to the build page

⚠️ Common mistake: The image tag must match your @playwright/test version exactly. A mismatch causes “Executable doesn’t exist” errors because the browsers in the image are a different build. See Playwright with Docker and Kubernetes.


🖥️ Option: Agent Without Docker

If your agent cannot run Docker, install Node.js (for example with the NodeJS plugin) and the browsers in the pipeline:

pipeline {
  agent { label 'linux' }
  tools { nodejs 'node-lts' }          // name configured in Manage Jenkins, Tools
  environment { CI = 'true' }
  stages {
    stage('Install') {
      steps {
        sh 'npm ci'
        sh 'npx playwright install --with-deps'   // needs root or sudo on Linux for system libraries
      }
    }
    stage('Test') {
      steps { sh 'npx playwright test' }
    }
  }
}

On a Windows agent use bat instead of sh, for example bat 'npx playwright test', and npx playwright install without --with-deps.


🔐 Step 3: Credentials

Store passwords and tokens in Manage Jenkins, Credentials, then expose them only to the test step:

stage('Test') {
  steps {
    withCredentials([usernamePassword(credentialsId: 'qa-test-user',
                                      usernameVariable: 'TEST_USER',
                                      passwordVariable: 'TEST_PASSWORD')]) {
      sh 'npx playwright test'
    }
  }
}

Your tests read process.env.TEST_USER and process.env.TEST_PASSWORD (environment variables). Jenkins masks them in the console log.


🎛️ Step 4: Parameters, Tags and Schedules

Let people choose what to run, and schedule nightly regression:

pipeline {
  agent { docker { image 'mcr.microsoft.com/playwright:v1.xx.x-noble'; args '--ipc=host' } }
  parameters {
    choice(name: 'SUITE', choices: ['@smoke', '@regression', 'all'], description: 'Which tests to run')
    choice(name: 'BROWSER', choices: ['chromium', 'firefox', 'webkit'], description: 'Browser project')
  }
  triggers { cron('H 2 * * *') }       // nightly, around 02:00 server time
  environment { CI = 'true' }
  stages {
    stage('Install') { steps { sh 'npm ci' } }
    stage('Test') {
      steps {
        script {
          def grep = params.SUITE == 'all' ? '' : "--grep '" + params.SUITE + "'"
          sh "npx playwright test --project=" + params.BROWSER + " " + grep
        }
      }
    }
  }
}

Tags are explained in group, tag and filter tests.


⚡ Step 5: Parallel Shards in Jenkins

Split a large suite into parallel stages with --shard (parallel execution):

stage('Test') {
  parallel {
    stage('Shard 1') { steps { sh 'npx playwright test --shard=1/3 --reporter=blob' } }
    stage('Shard 2') { steps { sh 'npx playwright test --shard=2/3 --reporter=blob' } }
    stage('Shard 3') { steps { sh 'npx playwright test --shard=3/3 --reporter=blob' } }
  }
}

When shards run on different agents, stash each blob-report folder under a different name, unstash them into one folder, and run npx playwright merge-reports --reporter html <folder> in a final stage.


🧾 Viewing the HTML Report in Jenkins

Jenkins serves published HTML with a strict Content Security Policy that blocks JavaScript. Because the Playwright HTML report is a JavaScript application, the Playwright Report link may show a blank page. Options:

  1. Download the archived playwright-report artifact and open it locally with npx playwright show-report <folder> (safest).
  2. Ask your Jenkins administrator to relax the CSP for HTML reports. This affects security for all published reports, so it is an admin decision.
  3. Rely on the JUnit test results in Jenkins for pass/fail and trends, and download traces only for failures (Trace Viewer).

✅ Jenkins Checklist

  • Jenkinsfile committed to the repository (pipeline as code).
  • Official Docker image with a tag matching @playwright/test, and --ipc=host.
  • CI = 'true' set in environment.
  • JUnit reporter for native results; HTML report and traces archived.
  • Secrets only through Jenkins Credentials.
  • Parameters for suite and browser; nightly cron trigger for regression.

📝 Assignment

  1. Add junit and html reporters to your config and a Jenkinsfile using the official Docker image.
  2. Create a Pipeline job in Jenkins pointing to your repository and run it.
  3. Break a test on purpose and find it in the Jenkins Test Result view and in the archived report.
  4. Move a password into Jenkins Credentials and use withCredentials.
  5. Add SUITE and BROWSER parameters and run only @smoke in Firefox.
  6. Add a nightly cron trigger.
  7. Bonus: split the suite into three parallel shard stages.

❓ FAQ

🤔 How do I run Playwright tests in Jenkins?

Create a Jenkinsfile that runs npm ci and npx playwright test, preferably inside the official mcr.microsoft.com/playwright Docker image, and publish JUnit results and the HTML report in the post section.

🤔 Why does the Playwright HTML report show a blank page in Jenkins?

Jenkins’ Content Security Policy blocks the report’s JavaScript. Download the archived report and open it locally, or have an administrator adjust the CSP.

🤔 Why does my Playwright config ignore CI settings in Jenkins?

Jenkins does not set CI=true by default. Add CI = 'true' to the pipeline’s environment block.

🤔 Do I need Docker to run Playwright in Jenkins?

No, but it is recommended. Without Docker, install Node.js on the agent and run npx playwright install --with-deps, which needs system packages on Linux.

🤔 How do I pass secrets to Playwright tests in Jenkins?

Store them in Jenkins Credentials and wrap the test step in withCredentials; read them in tests through process.env.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Run Playwright Tests in GitHub Actions: CI Workflow, Secrets, Sharding and Reports

Automation pays off when it runs on every change. In this lesson you will run your Playwright TypeScript suite in GitHub Actions, handle secrets, read reports and traces from CI, shard across parallel jobs, and schedule nightly regression runs.

📖 What You’ll Learn: the Playwright workflow YAML step by step, CI-friendly config, secrets and variables, downloading reports and traces, sharding with a matrix and merged report, running in the official Docker image, nightly and manual runs, and a CI checklist.


🚀 Why Run Playwright in CI?

Tests that only run on your laptop protect nobody. Continuous Integration (CI) runs your suite automatically on every push and pull request, so broken code is caught before it is merged. GitHub Actions is the most common choice for projects on GitHub, and Playwright supports it out of the box.

New to GitHub Actions itself? Start with the GitHub Actions complete roadmap and GitHub Actions fundamentals for beginners.


🧾 Step 1: The Basic Workflow

If you answered Yes to “Add a GitHub Actions workflow?” during installation, you already have this file. Otherwise create .github/workflows/playwright.yml:

name: Playwright Tests
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: lts/*

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps

      - name: Run Playwright tests
        run: npx playwright test

      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

What each step does:

StepPurpose
actions/checkoutDownloads your repository
actions/setup-nodeInstalls Node.js
npm ciInstalls the exact versions from package-lock.json
npx playwright install --with-depsDownloads browsers and Linux system libraries
npx playwright testRuns the suite
upload-artifactSaves the HTML report (with traces and screenshots) even when tests fail

⚠️ Common mistake: Always commit package-lock.json. npm ci fails without it, and it guarantees CI uses the same Playwright version as your machine.


⚙️ Step 2: CI-Friendly Config

The default config already reacts to process.env.CI, which GitHub sets automatically:

export default defineConfig({
  forbidOnly: !!process.env.CI,          // fail if test.only was committed
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: process.env.CI
    ? [['dot'], ['github'], ['html', { open: 'never' }]]
    : [['list'], ['html', { open: 'on-failure' }]],
  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
});

The github reporter adds annotations to the pull request, pointing at the exact failing lines.


🔐 Step 3: Secrets and Environment Variables

Never commit passwords or tokens. Add them under Settings, Secrets and variables, Actions in your repository, then pass them to the job:

      - name: Run Playwright tests
        run: npx playwright test
        env:
          BASE_URL: ${{ vars.BASE_URL }}
          TEST_USER: ${{ secrets.TEST_USER }}
          TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}

Read them in your config and tests with process.env.BASE_URL (environment variables). GitHub masks secret values in logs.


📥 Step 4: Read the Results

  1. Open the Actions tab and the workflow run.
  2. Failing tests appear as annotations in the summary and on the pull request.
  3. Download the playwright-report artifact, unzip it and run npx playwright show-report playwright-report.
  4. Open a failed test and its trace (Trace Viewer).

🧩 Step 5: Shard Across Parallel Jobs

For large suites, split tests across several machines with a matrix and merge the reports (parallel execution and sharding):

name: Playwright Tests (sharded)
on: [push, pull_request]

jobs:
  playwright-tests:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - run: npm ci
      - run: npx playwright install --with-deps
      - name: Run shard
        run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: blob-report-${{ matrix.shardIndex }}
          path: blob-report
          retention-days: 1

  merge-reports:
    if: ${{ !cancelled() }}
    needs: [playwright-tests]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - run: npm ci
      - uses: actions/download-artifact@v4
        with:
          path: all-blob-reports
          pattern: blob-report-*
          merge-multiple: true
      - name: Merge into one HTML report
        run: npx playwright merge-reports --reporter html ./all-blob-reports
      - uses: actions/upload-artifact@v4
        with:
          name: html-report--attempt-${{ github.run_attempt }}
          path: playwright-report
          retention-days: 14

🐳 Option: Run Inside the Official Playwright Docker Image

The official image already contains browsers and system libraries, which also gives consistent screenshots for visual tests:

jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/playwright:v1.xx.x-noble   # use the SAME version as @playwright/test in package.json
      options: --user 1001
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - run: npm ci
      - run: npx playwright test

📝 Note: The image tag must match your installed @playwright/test version, otherwise Playwright looks for browser builds that are not in the image. More in Playwright with Docker and Kubernetes.


⏰ Nightly and Manual Runs

Run the full cross-browser regression every night and allow manual runs from the Actions tab:

on:
  schedule:
    - cron: '30 20 * * *'      # 20:30 UTC = 02:00 IST
  workflow_dispatch:
    inputs:
      grep:
        description: 'Tag to run, e.g. @smoke'
        required: false
        default: ''

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      # ...checkout, setup-node, npm ci, install browsers...
      - run: npx playwright test --grep "${{ github.event.inputs.grep }}"

Combine with tags: @smoke on every pull request, the full suite nightly.


✅ CI Checklist

  • package-lock.json committed, npm ci in CI.
  • Browsers installed with --with-deps (or the official Docker image).
  • forbidOnly, retries and trace: 'on-first-retry' enabled for CI.
  • HTML report uploaded with if: !cancelled() so you get it when tests fail.
  • Secrets in GitHub Secrets, never in code.
  • Pull-request gate fast (smoke or Chromium only); full matrix nightly.

📝 Assignment

  1. Push your Sauce Demo project to a GitHub repository with the basic workflow and watch it run in the Actions tab.
  2. Break a test on purpose, push, and find the failure annotation and the trace in the downloaded report.
  3. Move the base URL into a repository variable and a password into a secret, and read them in the config.
  4. Add the github and dot reporters for CI only.
  5. Convert the workflow to 2 shards with a merged HTML report.
  6. Add a nightly schedule and a manual workflow_dispatch with a grep input.
  7. Bonus: run the job inside the official Playwright Docker image matching your version.

❓ FAQ

🤔 How do I run Playwright tests in GitHub Actions?

Add a workflow that checks out the code, sets up Node.js, runs npm ci, npx playwright install --with-deps and npx playwright test, then uploads playwright-report as an artifact.

🤔 How do I see the Playwright report from GitHub Actions?

Download the playwright-report artifact from the workflow run, unzip it and run npx playwright show-report <folder>, or publish it to GitHub Pages.

🤔 How do I use secrets in Playwright GitHub Actions?

Store them in the repository’s Actions secrets and pass them as env values with ${{ secrets.NAME }}; read them with process.env.NAME.

🤔 How do I run Playwright tests in parallel on GitHub Actions?

Use a matrix with --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} and the blob reporter, then merge the blob reports in a final job.

🤔 Should I cache Playwright browsers in GitHub Actions?

Usually not. Restoring a large cache takes about as long as downloading browsers, and the system dependencies still need installing. The official Docker image is a faster alternative.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Parallel Execution: Workers, fullyParallel and Sharding

Playwright runs tests in parallel for free, and with sharding you can split a suite across many machines. In this lesson you will learn how workers run tests, how to tune parallelism, how to write tests that are safe in parallel and how to shard and merge results.

📖 What You’ll Learn: how workers execute tests, configuring workers, fullyParallel and parallel mode, serial mode, parallel-safe test data, worker index and worker-scoped fixtures, sharding with blob reports, and a speed checklist.


⚡ Why Parallel Execution?

A suite of 300 UI tests at 10 seconds each takes 50 minutes in sequence. With 8 parallel workers it takes about 7 minutes, and split across 4 CI machines about 2 minutes. Fast feedback is what makes automation useful, and Playwright runs tests in parallel out of the box, for free.


👷 How Playwright Runs Tests in Parallel

Playwright starts several worker processes. Each worker is an independent OS process with its own browser.

  • By default, test files run in parallel across workers.
  • Tests inside one file run in order in the same worker.
  • Every test gets a fresh, isolated browser context, so tests do not share cookies or storage.
  • If a test fails, its worker is shut down and a new one starts for the remaining tests.
Running 40 tests using 4 workers
  worker 0: login.spec.ts     (5 tests in order)
  worker 1: cart.spec.ts      (8 tests in order)
  worker 2: checkout.spec.ts  (6 tests in order)
  worker 3: search.spec.ts    ...

🔢 Control the Number of Workers

// playwright.config.ts
export default defineConfig({
  workers: process.env.CI ? 2 : undefined,  // undefined = about half of the CPU cores
});
npx playwright test --workers=4
npx playwright test --workers=50%     # half of the CPU cores
npx playwright test --workers=1       # no parallelism (debugging)

💡 Tip: More workers is not always faster. Each worker runs a browser, which needs CPU and memory. On a 2-core CI runner, 2 workers is usually the sweet spot; measure before increasing.


🧵 Run Tests Inside a File in Parallel

To parallelize individual tests, not only files:

// Whole project
export default defineConfig({
  fullyParallel: true,
});
// One file only
import { test } from '@playwright/test';
test.describe.configure({ mode: 'parallel' });

test('search by name', async ({ page }) => { /* ... */ });
test('search by category', async ({ page }) => { /* ... */ });

With fullyParallel: true the workload is spread more evenly, especially when files have very different sizes.

Opting out: serial tests

Some flows must run in order (for example a multi-step wizard that shares state). Mark them serial; if one fails, the rest are skipped:

test.describe.configure({ mode: 'serial' });

Use serial mode sparingly: independent tests are faster and easier to debug (tags and grouping).


🧪 Write Tests That Are Safe to Run in Parallel

Parallel runs expose hidden dependencies. Follow these rules:

ProblemExampleSolution
Shared test dataTwo tests edit the same user profileCreate unique data per test
Shared account stateTests add and remove items in the same cartOne account per worker, or reset state per test
Order dependenceTest B expects test A’s order to existEach test creates what it needs (via API)
Global limitsThird-party sandbox allows one sessionMock it or run those tests serially

Use the worker index to give each worker its own data:

test('create a project', async ({ page }, testInfo) => {
  const name = 'Project-w' + testInfo.workerIndex + '-' + Date.now();
  // ...create and assert the project with a name no other worker uses
});

For per-worker accounts, a worker-scoped fixture creates one account per worker and reuses it for all its tests (custom fixtures):

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

export const test = base.extend<{}, { account: { username: string; password: string } }>({
  account: [async ({}, use, workerInfo) => {
    const username = 'qa_user_' + workerInfo.workerIndex;  // e.g. qa_user_0, qa_user_1 ...
    // create or reset this user via API here
    await use({ username, password: process.env.QA_PASSWORD ?? '' });
  }, { scope: 'worker' }],
});

🧩 Sharding: Split the Suite Across Machines

Sharding divides the tests into N parts and runs each part on a different machine (or CI job):

npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4

Each shard can still use several workers. Combine shards with the blob reporter and merge the results into one HTML report:

npx playwright test --shard=1/4 --reporter=blob
# ...after all shards...
npx playwright merge-reports --reporter html ./all-blob-reports

📝 Note: Enable fullyParallel: true when sharding, so tests (not whole files) are distributed and shards finish at similar times.

The complete GitHub Actions matrix setup is in Run Playwright tests in GitHub Actions, and merging is explained in reporters.


📏 Speed Checklist

  • Use fullyParallel: true for independent tests.
  • Log in once with a setup project and storageState (save login session) instead of logging in through the UI in every test.
  • Create test data through the API, not the UI (API testing).
  • Block heavy third-party resources in functional tests (network mocking).
  • Run the full browser matrix nightly, and Chromium only on pull requests (cross-browser).
  • Stop early on CI disasters with maxFailures: 10.

📝 Assignment

  1. Run your suite with --workers=1 and then --workers=4, and compare the total time.
  2. Enable fullyParallel: true and observe how tests from the same file now run in different workers (use --reporter=list).
  3. Log testInfo.workerIndex and testInfo.parallelIndex in a test and run with 3 workers.
  4. Make one file serial with test.describe.configure({ mode: 'serial' }), fail its first test on purpose, and observe the skipped tests.
  5. Run the suite as three shards in three terminals with --reporter=blob, then merge the reports.
  6. Bonus: create a worker-scoped fixture that returns a different username per worker.

❓ FAQ

🤔 Does Playwright run tests in parallel by default?

Yes. Test files run in parallel across worker processes. Tests inside a file run in order unless you set fullyParallel: true or mode: 'parallel'.

🤔 How do I set the number of workers in Playwright?

Use workers in the config (a number or a percentage such as '50%') or --workers=N on the command line.

🤔 How do I run Playwright tests sequentially?

Use --workers=1 for the whole run, or test.describe.configure({ mode: 'serial' }) for tests that must run in order.

🤔 What is sharding in Playwright?

Splitting the suite into parts with --shard=x/y so each part runs on a different machine, then merging the blob reports into one report.

🤔 Why do my tests fail only when run in parallel?

They share data or state, such as the same user, cart or record. Give each test or worker its own data, and avoid dependencies between tests.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Reporters and the HTML Report: Complete Guide

Test results are only useful if people can read them. In this lesson you will configure Playwright’s built-in reporters, get the most from the HTML report, produce JUnit and JSON for CI tools, merge sharded reports and write your own reporter.

📖 What You’ll Learn: what reporters do, every built-in reporter, configuring several reporters, local vs CI setups, the HTML report features, tags, annotations and attachments, merging blob reports from shards, third-party reporters, and writing a custom reporter.


📊 What Is a Reporter?

A reporter decides how Playwright presents test results: lines in the terminal, an interactive HTML page, a JUnit XML file for Jenkins, or a JSON file for your own dashboard. You can use several reporters at once, for example readable terminal output plus an HTML report plus JUnit XML for CI.


🧰 Built-in Reporters

ReporterOutputBest for
listOne line per test with status and timeLocal runs (default when no reporter is set)
lineA single updating line, failures printed in fullLarge suites in a terminal
dotOne character per testCI logs (default on CI when no reporter is set)
htmlInteractive web report in playwright-report/Humans: debugging, sharing results
jsonFull results as JSONDashboards, custom tooling
junitJUnit XMLJenkins, Azure DevOps, GitLab test tabs
githubAnnotations on the failing lines in GitHubGitHub Actions
blobRaw results for mergingSharded runs across machines

Choose one on the command line:

npx playwright test --reporter=list
npx playwright test --reporter=dot
npx playwright test --reporter=line,html

⚙️ Configure Reporters in playwright.config.ts

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

export default defineConfig({
  reporter: [
    ['list'],                                                    // terminal
    ['html', { outputFolder: 'playwright-report', open: 'never' }], // HTML report
    ['junit', { outputFile: 'results/junit.xml' }],              // for CI test tabs
    ['json', { outputFile: 'results/results.json' }],            // for dashboards
  ],
});

A common pattern is different reporters locally and in CI:

reporter: process.env.CI
  ? [['dot'], ['github'], ['html', { open: 'never' }], ['junit', { outputFile: 'results/junit.xml' }]]
  : [['list'], ['html', { open: 'on-failure' }]],

The HTML open option accepts 'always', 'never' or 'on-failure' (the default opens the report automatically only when tests fail locally).


🌐 The HTML Report in Depth

npx playwright show-report                       # open the last report
npx playwright show-report my-report-folder      # open a report from another folder
npx playwright show-report --port 9323           # choose the port

What you get:

  • Summary of passed, failed, flaky and skipped tests, with filters and search.
  • Results grouped by file and project (browser).
  • Tags and annotations shown on each test (tags, annotations).
  • Every step, including test.step groups, hooks and fixtures, with durations.
  • Errors with code snippets, screenshots, videos and traces (screenshots and videos, Trace Viewer).
  • Attachments you added with testInfo.attach().

The report is a static website. You can zip it, upload it as a CI artifact, or publish it to any static hosting so the whole team can open it in a browser.

Add useful information to the report

test('checkout', {
  tag: '@smoke',
  annotation: { type: 'issue', description: 'https://jira.example.com/browse/QA-42' },
}, async ({ page }, testInfo) => {
  await test.step('open the cart', async () => {
    // ...
  });
  await testInfo.attach('order-details', { body: JSON.stringify({ orderId: 123 }), contentType: 'application/json' });
});

🧩 Merge Reports from Sharded Runs

When you split a suite across several CI machines with --shard, each machine produces a blob report. Merge them into one HTML report:

# on each machine
npx playwright test --shard=1/3 --reporter=blob
# after all shards finish, with all blob-report folders copied into one folder
npx playwright merge-reports --reporter html ./all-blob-reports

The full CI setup is in parallel execution and sharding and GitHub Actions.


🧑‍🔧 Third-Party Reporters

The community offers many reporters. The most popular in QA teams is Allure, with history, trends and categories; see Allure report integration. Others integrate with test management tools and dashboards. Install them from npm and add them to the reporter array like built-in ones.


✍️ Write Your Own Reporter

A custom reporter is a class with optional hooks. This one prints a short summary and the slowest tests:

// reporters/summary-reporter.ts
import type { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';

class SummaryReporter implements Reporter {
  private durations: { title: string; ms: number }[] = [];

  onBegin(config: FullConfig, suite: Suite) {
    console.log('Starting ' + suite.allTests().length + ' tests');
  }

  onTestEnd(test: TestCase, result: TestResult) {
    this.durations.push({ title: test.title, ms: result.duration });
    if (result.status === 'failed') console.log('FAILED: ' + test.title);
  }

  onEnd(result: FullResult) {
    const slowest = [...this.durations].sort((a, b) => b.ms - a.ms).slice(0, 3);
    console.log('Finished with status: ' + result.status);
    slowest.forEach(t => console.log('  slow: ' + t.title + ' (' + t.ms + ' ms)'));
  }
}

export default SummaryReporter;
// playwright.config.ts
reporter: [['list'], ['./reporters/summary-reporter.ts']],

Custom reporters are useful for sending results to Slack or Teams, updating a test management tool, or enforcing rules such as “fail the build if any test took longer than 60 seconds”.


✅ Reporting Best Practices

  • Locally: list plus html that opens on failure.
  • In CI: a compact terminal reporter (dot or line), html uploaded as an artifact, junit for the CI test tab, and github annotations on GitHub.
  • Keep reports out of Git (playwright-report/ and test-results/ are already in the default .gitignore).
  • Use tags, annotations and test.step so reports explain themselves.
  • Attach screenshots, videos and traces only for failures to keep reports small.

📝 Assignment

  1. Run your suite with --reporter=list, --reporter=line and --reporter=dot and compare the output.
  2. Configure list, html (open: 'on-failure') and junit together, run the suite, and find all three outputs.
  3. Add a tag, an issue annotation, two test.step blocks and a JSON attachment to one test, then view it in the HTML report.
  4. Add the JSON reporter and read the number of passed tests from the file with a small Node script.
  5. Write the SummaryReporter above, register it, and print the three slowest tests.
  6. Bonus: run the suite as two shards with --reporter=blob and merge them into one HTML report.

❓ FAQ

🤔 How do I generate an HTML report in Playwright?

Add reporter: 'html' (or ['html', { open: 'never' }]) in the config and run the tests. Open it with npx playwright show-report.

🤔 How do I use multiple reporters in Playwright?

Pass an array: reporter: [['list'], ['html'], ['junit', { outputFile: 'results.xml' }]].

🤔 How do I get JUnit XML results for Jenkins?

Add ['junit', { outputFile: 'results/junit.xml' }] to the reporters and point Jenkins’ JUnit plugin at that file. See Playwright with Jenkins.

🤔 How do I merge Playwright reports from shards?

Run each shard with --reporter=blob, collect the blob-report folders, then run npx playwright merge-reports --reporter html <folder>.

🤔 Can I create a custom reporter in Playwright?

Yes. Implement the Reporter interface from @playwright/test/reporter (onBegin, onTestEnd, onEnd and more) and add the file path to the reporter array.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Trace Viewer Step by Step: Record, Open and Debug Traces

The Trace Viewer turns a mysterious CI failure into a step-by-step replay you can inspect like DevTools. In this lesson you will configure tracing, open traces in several ways, learn every panel of the viewer and follow a real debugging walkthrough.

📖 What You’ll Learn: what a trace contains, trace configuration values, opening traces from the report, CLI and trace.playwright.dev, every Trace Viewer panel, a CI debugging walkthrough, test.step, tracing in plain scripts, and handling traces in CI safely.


🕵️ What Is a Playwright Trace?

A trace is a complete recording of a test run that you can open after the fact. Unlike a video, it is interactive: for every action you can see the DOM before and after, the exact element that was targeted, network requests, console messages and the source line that ran.

That makes the Trace Viewer the best tool for the hardest failures: the ones that only happen in CI, at night, on someone else’s machine. Instead of guessing, you replay the run step by step.


⚙️ Turn On Tracing

Configure tracing once in playwright.config.ts:

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

export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    trace: 'on-first-retry',
  },
});
ValueWhen a trace is recorded or kept
'off'Never (default)
'on'For every test (heavy, use for debugging sessions)
'retain-on-failure'Recorded for every test, kept only for failures
'on-first-retry'Only on the first retry of a failed test (recommended with retries in CI)
'on-all-retries'On every retry
'retain-on-first-failure'Kept only for the first failed attempt

💡 Tip: Use 'on-first-retry' in CI when retries are enabled, or 'retain-on-failure' when they are not. Both give you a trace for every failure without slowing down passing tests much.

Force tracing for one run from the command line:

npx playwright test tests/cart.spec.ts --trace on

📂 Open a Trace

Traces are saved as trace.zip files inside test-results/. Open them in any of these ways:

  1. From the HTML report: run npx playwright show-report, open the failed test, and click the trace image or link.
  2. From the command line: npx playwright show-trace test-results/<test-folder>/trace.zip
  3. In the browser: drag the zip onto trace.playwright.dev. The viewer runs entirely in your browser; the file is not uploaded to a server.
  4. In UI mode (--ui) and the VS Code extension (enable Show trace viewer), traces open automatically.

🧭 Tour of the Trace Viewer

AreaWhat it showsHow it helps
Actions (left)Every Playwright action and assertion, with durationFind the failing step and slow steps
Timeline (top)Filmstrip of screenshots over timeSee what the user would have seen
Snapshots (centre)DOM snapshots: Before, Action, AfterInspect the real page at that moment
Locator pickerPick or type a locator against the snapshotFix locators against the exact failing DOM
CallParameters, locator and timing of the actionCheck what Playwright actually did
LogActionability checks for the actionSee “waiting for element to be visible” and similar
ErrorsError messages and stackJump to the failure
ConsoleBrowser console logs and page errorsSpot JavaScript errors in the app
NetworkEvery request with status, timing, headers and bodyFind failed or slow API calls
SourceYour test code, with the current line highlightedConnect the step to your code
AttachmentsScreenshots and files attached in the testExtra evidence

Reading the “Action” snapshot

Click a click-step and choose the Action tab: a red dot shows exactly where Playwright clicked. If the dot is on an overlay or the wrong element, you have found the bug.


🧪 A Debugging Walkthrough

Imagine this test fails only in CI with “Timeout 30000ms exceeded” on click():

test('checkout', async ({ page }) => {
  // ...log in, add item...
  await page.getByRole('button', { name: 'Checkout' }).click();
});

In the trace:

  1. Actions: the click step is red and took 30 seconds.
  2. Log: the element is visible, enabled and stable, followed by repeated “another element would receive the click”.
  3. Snapshot (Before): a cookie consent banner covers the bottom of the page in the CI viewport.
  4. Network: the banner script loaded only in CI because a feature flag differed.

Fix: accept or close the banner in a beforeEach (or set the consent cookie), and align the viewport with production. No guessing, no “works on my machine”.


🏷️ Make Traces Easier to Read with test.step

Group actions into named steps; they appear as collapsible sections in the trace and the HTML report:

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

test('purchase a backpack', async ({ page }) => {
  await test.step('log in', async () => {
    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 test.step('add backpack to cart', async () => {
    await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
    await expect(page.locator('.shopping_cart_badge')).toHaveText('1');
  });
});

🛠️ Record Traces Outside the Test Runner

In plain Playwright library scripts, start and stop tracing on the context:

import { chromium } from 'playwright';

(async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  await context.tracing.start({ screenshots: true, snapshots: true, sources: true });

  const page = await context.newPage();
  await page.goto('https://www.saucedemo.com/');

  await context.tracing.stop({ path: 'trace.zip' });
  await browser.close();
})();

☁️ Traces in CI

To use traces from CI runs, upload the report folder as a build artifact (shown in full in the GitHub Actions and Jenkins lessons):

- uses: actions/upload-artifact@v4
  if: ${{ !cancelled() }}
  with:
    name: playwright-report
    path: playwright-report/
    retention-days: 14

Download the artifact, open the HTML report, and click the trace of the failed test.

⚠️ Common mistake: Traces contain everything the page saw, including cookies, tokens and form values. Treat trace files like logs with sensitive data: limit who can download CI artifacts and use test accounts only.


📝 Assignment

  1. Set trace: 'retain-on-failure', break one Sauce Demo test, and open its trace from the HTML report.
  2. In the trace, find the failing action, read its Log, and inspect the Before and After snapshots.
  3. Use the locator picker in the trace to find a working locator for the element you broke.
  4. Open the Network tab and list the requests made when the inventory page loaded.
  5. Wrap your test in two or three test.step blocks and see how the trace groups actions.
  6. Open the same trace with npx playwright show-trace and on trace.playwright.dev.
  7. Bonus: record a trace from a plain Node script with context.tracing.start().

❓ FAQ

🤔 What is the Playwright Trace Viewer?

A GUI tool that replays a recorded test run action by action, with DOM snapshots, network requests, console logs and source code, so you can debug failures after they happened.

🤔 How do I open a Playwright trace?

From the HTML report, with npx playwright show-trace path/to/trace.zip, or by dropping the file onto trace.playwright.dev.

🤔 Which trace setting should I use in CI?

'on-first-retry' when retries are enabled, or 'retain-on-failure' otherwise. Both keep traces for failures without recording every passing test permanently.

🤔 Is trace.playwright.dev safe for private traces?

The viewer loads the file locally in your browser and does not upload it. Still follow your company’s policy for handling files that contain sensitive data.

🤔 What is the difference between a trace and a video?

A video only shows pixels. A trace lets you inspect the DOM, locators, network, console and source for every step, which makes root-cause analysis much faster.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Debugging Playwright Tests: Inspector, page.pause, UI Mode and VS Code

Every automation engineer spends time debugging. Playwright makes it fast with excellent error messages, the Inspector, UI mode and VS Code integration. In this lesson you will learn a step-by-step debugging workflow and the right tool for each kind of failure.

📖 What You’ll Learn: reading errors and call logs, the Playwright Inspector with –debug, page.pause, UI mode, VS Code breakpoints, slow motion and headed runs, capturing console and network errors, DEBUG logs, and a symptom-to-tool troubleshooting table.


🐞 A Debugging Mindset

When a Playwright test fails, ask three questions in order:

  1. What did the error say? Playwright errors include the locator, the expected and received values, and a call log of what it tried.
  2. What did the page look like? Use screenshots, UI mode or the trace.
  3. What happened step by step? Step through the test with the Inspector or a breakpoint.

Playwright gives you a tool for each question. This lesson covers all of them; the most powerful one, the Trace Viewer, has its own lesson.


📜 Step 1: Read the Error Message and Call Log

Error: expect(locator).toHaveText(expected) failed

Locator: locator('.title')
Expected string: "Product"
Received string: "Products"
Timeout: 5000ms

Call log:
  - Expect "toHaveText" with timeout 5000ms
  - waiting for locator('.title')
    9 × locator resolved to <span class="title" data-test="title">Products</span>
      - unexpected value "Products"

The call log shows that the element was found nine times and its text was always “Products”. The test, not the app, is wrong. For actions you will see entries such as “element is not visible” or “another element would receive the click”, which point straight at the cause.


🧭 Step 2: Playwright Inspector with –debug

Run a test in debug mode:

npx playwright test tests/login.spec.ts --debug           # debug a file
npx playwright test tests/login.spec.ts:12 --debug        # debug the test on line 12
npx playwright test -g "locked out user" --debug          # debug by title

A headed browser and the Playwright Inspector open, paused before the first action. In the Inspector you can:

ControlWhat it does
Step overRun the next action only
ResumeContinue until the next page.pause() or the end
Pick locatorClick an element to see the best locator for it
Locator boxEdit a locator and see live which elements it highlights
Call logWatch Playwright’s actionability checks for the current step

The element the next action targets is highlighted in the browser, so you immediately see whether the locator points at the right element.

💡 Tip: Use the locator box to fix a broken locator before editing the code: type your new locator and check that exactly one element is highlighted.


⏸️ Step 3: Pause Exactly Where You Need with page.pause()

Add await page.pause() anywhere in a test and run it headed. Execution stops there with the Inspector open:

test('checkout total', 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();   // inspect the products page here

  await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
});
npx playwright test tests/checkout.spec.ts --headed

Remove page.pause() before committing; with forbidOnly-style discipline, a lint rule such as playwright/no-page-pause catches leftovers.


🖥️ Step 4: UI Mode for Day-to-Day Debugging

npx playwright test --ui

UI mode is the most comfortable way to develop and debug tests:

  • Run any test, file or tag with one click; watch mode re-runs on save.
  • A timeline of every action with before and after DOM snapshots you can inspect like DevTools.
  • Network, console, errors and source tabs for the selected step.
  • Pick locator directly on the snapshot.

Most “why did it click the wrong thing?” questions are answered by clicking the step in the timeline and looking at the highlighted element.


🧑‍💻 Step 5: Debug in VS Code with Breakpoints

With the Playwright Test for VSCode extension:

  1. Click in the gutter next to a line to set a breakpoint.
  2. Right-click the green ▶ next to the test and choose Debug Test.
  3. Execution stops at the breakpoint; hover variables, use the Debug Console, step with F10.
  4. Tick Show browser to watch the page; while paused, editing a locator in the code highlights the matching element in the browser.

This combines normal TypeScript debugging (variables, call stack) with live browser feedback.


🐢 Step 6: Slow Down and Watch

Sometimes you simply need to see what happens:

npx playwright test --headed --workers=1 --project=chromium
// playwright.config.ts: slow every action down by 500 ms (debugging only)
use: {
  launchOptions: { slowMo: 500 },
},

⚠️ Common mistake: Never leave slowMo or headless: false in the shared config; they make CI slow. Put them in a local-only config or pass --headed on the command line.


📢 Step 7: Capture Browser Console and Network Errors

Front-end JavaScript errors often explain UI failures. Print them during the test:

test.beforeEach(async ({ page }) => {
  page.on('console', msg => {
    if (msg.type() === 'error') console.log('Browser console error:', msg.text());
  });
  page.on('pageerror', err => console.log('Uncaught page error:', err.message));
  page.on('requestfailed', req => console.log('Request failed:', req.url(), req.failure()?.errorText));
});

For very detailed Playwright internals, enable debug logs:

# macOS / Linux
DEBUG=pw:api npx playwright test tests/login.spec.ts

# Windows PowerShell
$env:DEBUG='pw:api'; npx playwright test tests/login.spec.ts

🧰 Debugging Toolbox Summary

ToolBest forCommand
Error + call logFirst look at any failureAlways printed
InspectorStepping through actions, fixing locators--debug
page.pause()Stopping at one exact pointAdd to code, run --headed
UI modeDeveloping and exploring tests--ui
VS Code debuggerVariables, logic, breakpointsDebug Test
Trace ViewerFailures in CI, after the facttrace: 'on-first-retry'
Screenshots and videoQuick visual evidencescreenshot, video in config

🔎 Common Failures and Where to Look

SymptomLikely causeDebug with
“strict mode violation”Locator matches several elementsInspector locator box
“element is not visible”Hidden, wrong tab, not yet renderedUI mode snapshot
“intercepts pointer events”Overlay, modal or cookie bannerUI mode snapshot, trace
Assertion timeout with wrong valueTest data or app logicError + call log
Passes locally, fails in CITiming, environment, dataTrace Viewer, flaky tests

📝 Assignment

  1. Break a locator in one of your Sauce Demo tests on purpose, read the error and call log, and write down what they told you.
  2. Run the test with --debug, step through it, and fix the locator using the Inspector’s locator box.
  3. Add page.pause() after login, run headed, and use Pick locator on three elements.
  4. Open UI mode, run your suite, and inspect the before and after snapshots of a click.
  5. Set a breakpoint in VS Code inside a test, run Debug Test, and inspect a variable.
  6. Add console and page-error listeners in a beforeEach and visit a page that logs errors.
  7. Bonus: run one test with DEBUG=pw:api and find the line where Playwright waits for the element to be visible.

❓ FAQ

🤔 How do I debug a Playwright test?

Run it with npx playwright test --debug to open the Inspector, use --ui for UI mode, add await page.pause() to stop at a point, or use Debug Test in VS Code with breakpoints.

🤔 What does page.pause() do in Playwright?

It stops the test and opens the Playwright Inspector so you can explore the page, pick locators and continue step by step. It only works in headed mode.

🤔 How do I run Playwright in slow motion?

Set launchOptions: { slowMo: 500 } in the config (for local debugging only), or use --debug and step through actions.

🤔 How do I see browser console logs in Playwright?

Listen to the console event: page.on('console', msg => console.log(msg.text())), and to pageerror for uncaught exceptions.

🤔 What is the difference between UI mode and the Trace Viewer?

UI mode runs tests interactively on your machine with a live timeline. The Trace Viewer opens recorded traces after a run, which is ideal for failures that happened in CI.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Framework Folder Structure: Best Practices with TypeScript

A good folder structure is the difference between a framework that scales and one that collapses after 200 tests. In this lesson you will build a proven Playwright + TypeScript framework layout, understand each layer, and apply naming and quality rules used in real teams.

📖 What You’ll Learn: why structure matters, a recommended folder layout, the framework layers and their rules, example page objects, data, fixtures and tests, tsconfig path aliases, environment configuration, naming conventions, code quality tooling and a framework checklist.


🏗️ Why Folder Structure Matters

A Playwright project with 20 tests can live in one folder. A project with 2,000 tests, five contributors and three environments cannot. A clear structure gives you:

  • Findability: anyone can guess where a page object, a fixture or a data file lives.
  • Reuse: login, API clients and helpers are written once.
  • Safe change: a UI change is fixed in one page object, not in 40 tests.
  • Easy onboarding: new team members follow existing patterns.

This lesson presents a proven structure for Playwright + TypeScript frameworks, explains every folder, and shows how the layers connect.


📁 Recommended Folder Structure

playwright-framework/
├── .github/workflows/
│   └── playwright.yml          # CI pipeline
├── config/
│   ├── env.ts                  # reads and validates environment variables
│   └── environments.ts         # URLs per environment (dev, qa, staging)
├── src/
│   ├── pages/                  # Page Object Model
│   │   ├── BasePage.ts
│   │   ├── LoginPage.ts
│   │   ├── InventoryPage.ts
│   │   └── components/
│   │       └── HeaderComponent.ts
│   ├── api/                    # API clients for setup and API tests
│   │   ├── ApiClient.ts
│   │   └── UsersApi.ts
│   ├── fixtures/
│   │   └── index.ts            # custom test with pages, API clients, test data
│   ├── data/                   # static test data
│   │   ├── users.ts
│   │   └── products.json
│   ├── utils/                  # pure helpers (dates, random data, formatting)
│   │   └── random.ts
│   └── types/                  # shared interfaces
│       └── user.ts
├── tests/
│   ├── auth.setup.ts           # logs in once, saves storageState
│   ├── ui/
│   │   ├── login.spec.ts
│   │   └── cart.spec.ts
│   ├── api/
│   │   └── users.api.spec.ts
│   └── visual/
│       └── home.visual.spec.ts
├── playwright/.auth/           # saved sessions (git-ignored)
├── .env.example                # documented variables, no secrets
├── .gitignore
├── eslint.config.mjs
├── package.json
├── playwright.config.ts
└── tsconfig.json

💡 Tip: Keep framework code (src/) separate from tests (tests/). Tests should read like user stories; all the “how” lives in src/.


🧱 The Layers and How They Connect

Each layer only talks to the layer below it:

LayerFolderResponsibilityKnows about
Teststests/What to verify, written as user flowsFixtures
Fixturessrc/fixtures/Create and inject page objects, API clients, dataPages, API, data
Pages and componentssrc/pages/Locators and user actions for one page or widgetPlaywright Page
API clientssrc/api/HTTP calls for setup, cleanup and API testsPlaywright APIRequestContext
Data, types, utilssrc/data/, src/types/, src/utils/Static data, interfaces, pure helpersNothing above
Configconfig/, playwright.config.tsEnvironments, browsers, timeouts, reportersEnvironment variables

Rules that keep the framework clean:

  1. Tests never contain raw selectors; they call page-object methods.
  2. Page objects never contain assertions about business rules; tests do (simple “is the page loaded” checks are fine).
  3. Utilities are pure functions with no Playwright dependency.
  4. Secrets come only from environment variables.

📄 Example Files

src/pages/BasePage.ts

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

export abstract class BasePage {
  constructor(protected readonly page: Page) {}
  protected abstract readonly path: string;

  async open(): Promise<void> {
    await this.page.goto(this.path);
  }
}

src/pages/LoginPage.ts

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

export class LoginPage extends BasePage {
  protected readonly path = '/';
  readonly username: Locator;
  readonly password: Locator;
  readonly submit: Locator;
  readonly error: Locator;

  constructor(page: Page) {
    super(page);
    this.username = page.getByPlaceholder('Username');
    this.password = page.getByPlaceholder('Password');
    this.submit = page.getByRole('button', { name: 'Login' });
    this.error = page.locator('[data-test="error"]');
  }

  async login(user: { username: string; password: string }): Promise<void> {
    await this.username.fill(user.username);
    await this.password.fill(user.password);
    await this.submit.click();
  }
}

src/types/user.ts and src/data/users.ts

// src/types/user.ts
export interface User { username: string; password: string }

// src/data/users.ts
import { User } from '../types/user';

export const users = {
  standard: { username: 'standard_user', password: process.env.SAUCE_PASSWORD ?? 'secret_sauce' },
  locked:   { username: 'locked_out_user', password: process.env.SAUCE_PASSWORD ?? 'secret_sauce' },
} satisfies Record<string, User>;

src/fixtures/index.ts

import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { InventoryPage } from '../pages/InventoryPage';

type Fixtures = {
  loginPage: LoginPage;
  inventoryPage: InventoryPage;
};

export const test = base.extend<Fixtures>({
  loginPage: async ({ page }, use) => { await use(new LoginPage(page)); },
  inventoryPage: async ({ page }, use) => { await use(new InventoryPage(page)); },
});
export { expect } from '@playwright/test';

tests/ui/login.spec.ts

import { test, expect } from '../../src/fixtures';
import { users } from '../../src/data/users';

test.describe('Login', { tag: '@smoke' }, () => {
  test('standard user reaches the products page', async ({ loginPage, page }) => {
    await loginPage.open();
    await loginPage.login(users.standard);
    await expect(page).toHaveURL(/inventory/);
  });

  test('locked user sees an error', async ({ loginPage }) => {
    await loginPage.open();
    await loginPage.login(users.locked);
    await expect(loginPage.error).toContainText('locked out');
  });
});

The test is short, readable and free of selectors. Learn each building block in depth: Page Object Model, custom fixtures, API with POM.


🔗 Cleaner Imports with Path Aliases

Long relative paths like ../../src/pages/LoginPage are hard to read. Define aliases in tsconfig.json; Playwright resolves them automatically:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "commonjs",
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "@pages/*": ["src/pages/*"],
      "@fixtures": ["src/fixtures/index.ts"],
      "@data/*": ["src/data/*"]
    }
  }
}
import { test, expect } from '@fixtures';
import { users } from '@data/users';

🌍 Environments and Config

Keep environment-specific values out of the code:

// config/environments.ts
export const environments = {
  dev:     'https://dev.example.com',
  qa:      'https://qa.example.com',
  staging: 'https://staging.example.com',
} as const;

export type EnvName = keyof typeof environments;
// playwright.config.ts (excerpt)
import 'dotenv/config';
import { defineConfig } from '@playwright/test';
import { environments, EnvName } from './config/environments';

const env = (process.env.TEST_ENV ?? 'qa') as EnvName;

export default defineConfig({
  use: { baseURL: environments[env] },
});

Run against another environment with TEST_ENV=staging npx playwright test (on PowerShell: $env:TEST_ENV='staging'; npx playwright test). Details in .env files and multiple config files.


📏 Naming Conventions

ItemConventionExample
UI test filesfeature.spec.tscheckout.spec.ts
API test filesfeature.api.spec.tsusers.api.spec.ts
Setup filesname.setup.tsauth.setup.ts
Page objectsPascalCase + PageCheckoutPage.ts
ComponentsPascalCase + ComponentHeaderComponent.ts
Test titlesBehaviour, not steps'guest can apply a discount code'
Locator propertiesNoun describing the elementsubmitButton, cartBadge
Action methodsVerb describing the user intentlogin(), addToCart()

🧹 Code Quality Tooling

  • ESLint with typescript-eslint and eslint-plugin-playwright (rules such as no-floating-promises and playwright/no-wait-for-timeout).
  • Prettier for consistent formatting.
  • Type check in CI: npx tsc --noEmit (TypeScript for Playwright).
  • Pre-commit hook (Husky + lint-staged) to lint changed files.
  • .env.example listing every variable without real values.

✅ Framework Checklist

  • Tests contain no selectors and no hard-coded URLs or secrets.
  • Every page and reusable widget has one class.
  • Login runs once per run through a setup project with storageState.
  • Test data is typed and lives in src/data or is generated per test.
  • One command runs everything locally the same way CI does.
  • CI uploads the HTML report and traces on failure.

📝 Assignment

  1. Create the folder structure above for Sauce Demo (you can leave some folders empty).
  2. Move your login and cart tests into tests/ui/ and create LoginPage, InventoryPage and BasePage in src/pages/.
  3. Create src/fixtures/index.ts that injects both page objects, and update the tests to use it.
  4. Add path aliases @pages/*, @fixtures and @data/* and replace the relative imports.
  5. Add config/environments.ts and select the base URL with TEST_ENV.
  6. Install ESLint with the Playwright plugin and fix every warning.
  7. Bonus: add an auth.setup.ts setup project that logs in once and saves storageState.

❓ FAQ

🤔 What is the best folder structure for a Playwright framework?

Separate tests (tests/) from framework code (src/ with pages, API clients, fixtures, data, types and utils), keep configuration in config/ and playwright.config.ts, and store sessions and reports in git-ignored folders.

🤔 Where should page objects go in Playwright?

In a dedicated folder such as src/pages/, with one class per page and a components/ subfolder for reusable widgets.

🤔 Should Playwright UI tests and API tests be in separate folders?

Yes. Separate folders (and usually separate projects in the config) make it easy to run them independently and give them different settings.

🤔 Can I use TypeScript path aliases in Playwright?

Yes. Define paths in tsconfig.json; Playwright Test resolves them automatically.

🤔 How do I switch environments in a Playwright framework?

Store URLs per environment in a config file and choose one with an environment variable such as TEST_ENV, read in playwright.config.ts.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

API Testing with Playwright and TypeScript: GET, POST, PUT, PATCH, DELETE

Playwright is not only for browsers: it includes a complete HTTP client for API testing. In this lesson you will test a REST API with GET, POST, PUT, PATCH and DELETE requests, handle authentication, type your responses and combine API calls with UI tests.

📖 What You’ll Learn: why API testing in Playwright, an API project with baseURL, the request fixture, GET with params, POST, PUT, PATCH and DELETE, request options and response methods, authentication with tokens, typed responses, and combining API setup with UI checks.


🔌 Why API Testing with Playwright?

Most applications are a user interface on top of REST APIs. Testing those APIs directly is fast, stable and catches backend bugs early. Playwright includes a full HTTP client, so you can write API tests in the same framework, language and report as your UI tests, with no Postman collection or separate library to maintain.

API tests in Playwright:

  • Run in milliseconds, without opening a browser.
  • Use the same expect assertions, fixtures, tags, retries and HTML report.
  • Can be combined with UI tests, for example to create test data via API before checking it in the browser.

We will use JSONPlaceholder (https://jsonplaceholder.typicode.com), a free fake REST API. It accepts POST, PUT, PATCH and DELETE requests and returns realistic responses, but does not really save changes.


⚙️ Setup: baseURL for API Tests

Set the API address once in playwright.config.ts. A separate project keeps API tests apart from browser tests:

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

export default defineConfig({
  projects: [
    {
      name: 'api',
      testMatch: /.*\.api\.spec\.ts/,
      use: {
        baseURL: 'https://jsonplaceholder.typicode.com',
        extraHTTPHeaders: { Accept: 'application/json' },
      },
    },
    { name: 'chromium', testIgnore: /.*\.api\.spec\.ts/, use: { ...devices['Desktop Chrome'] } },
  ],
});
npx playwright test --project=api

📥 GET: Read Data

The built-in request fixture is an APIRequestContext. Relative URLs use the baseURL:

// tests/posts.api.spec.ts
import { test, expect } from '@playwright/test';

test('GET a single post', async ({ request }) => {
  const response = await request.get('/posts/1');

  expect(response.status()).toBe(200);
  await expect(response).toBeOK();               // any 2xx status

  const post = await response.json();
  expect(post).toMatchObject({ id: 1, userId: 1 });
  expect(typeof post.title).toBe('string');
});

test('GET with query parameters', async ({ request }) => {
  const response = await request.get('/posts', { params: { userId: 1 } });
  await expect(response).toBeOK();

  const posts = await response.json();
  expect(posts).toHaveLength(10);
  for (const p of posts) {
    expect(p.userId).toBe(1);
  }
});

📤 POST: Create Data

test('POST creates a post', async ({ request }) => {
  const newPost = { title: 'Learn Playwright API testing', body: 'It is fast!', userId: 1 };

  const response = await request.post('/posts', { data: newPost });

  expect(response.status()).toBe(201);
  const created = await response.json();
  expect(created).toMatchObject(newPost);
  expect(created.id).toEqual(expect.any(Number));
});

data is sent as JSON automatically when you pass an object. For HTML forms use form: { ... }, and for file uploads use multipart: { ... }.


✏️ PUT and PATCH: Update Data

test('PUT replaces a post', async ({ request }) => {
  const response = await request.put('/posts/1', {
    data: { id: 1, title: 'Updated title', body: 'Updated body', userId: 1 },
  });
  await expect(response).toBeOK();
  expect((await response.json()).title).toBe('Updated title');
});

test('PATCH updates one field', async ({ request }) => {
  const response = await request.patch('/posts/1', { data: { title: 'Only the title changed' } });
  await expect(response).toBeOK();
  expect((await response.json()).title).toBe('Only the title changed');
});

PUT replaces the whole resource; PATCH changes only the fields you send.


🗑️ DELETE: Remove Data

test('DELETE removes a post', async ({ request }) => {
  const response = await request.delete('/posts/1');
  expect(response.status()).toBe(200);
});

🧰 Request Options and Response Methods

Request optionPurpose
paramsQuery string, e.g. { userId: 1 }
dataJSON body (object) or raw string
formURL-encoded form body
multipartMultipart body with files
headersExtra headers for this request
timeoutRequest timeout in ms
failOnStatusCodeThrow automatically if the status is not 2xx or 3xx
Response methodReturns
status()Status code, e.g. 200
ok()true for 200 to 299
json()Parsed JSON body
text()Body as a string
headers()Response headers as an object
body()Raw body as a Buffer

🔐 Authentication: Tokens and Headers

Real APIs need authentication. Keep secrets in environment variables (.env files) and send them as headers:

// For every request in the project
use: {
  baseURL: process.env.API_URL,
  extraHTTPHeaders: { Authorization: 'Bearer ' + process.env.API_TOKEN },
},

Or log in once and create an authenticated context yourself:

import { test, expect, request as playwrightRequest, APIRequestContext } from '@playwright/test';

let api: APIRequestContext;

test.beforeAll(async () => {
  const auth = await playwrightRequest.newContext({ baseURL: process.env.API_URL });
  const login = await auth.post('/auth/login', { data: { username: process.env.API_USER, password: process.env.API_PASSWORD } });
  const { token } = await login.json();

  api = await playwrightRequest.newContext({
    baseURL: process.env.API_URL,
    extraHTTPHeaders: { Authorization: 'Bearer ' + token },
  });
});

test.afterAll(async () => {
  await api.dispose();
});

test('get my profile', async () => {
  const response = await api.get('/me');
  await expect(response).toBeOK();
});

🧾 Type Your API Responses

Give responses a TypeScript type so your editor autocompletes fields and catches typos (interfaces, generics):

interface Post { userId: number; id: number; title: string; body: string }

test('typed response', async ({ request }) => {
  const post: Post = await (await request.get('/posts/1')).json();
  expect(post.title.length).toBeGreaterThan(0);
});

Types do not validate data at runtime. For strict contract checks, combine toMatchObject and expect.any(Number) as above, or add a JSON-schema library.


🔗 Combine API and UI Tests

The real power: use the API for setup and the browser for verification. page.request shares cookies with the browser page, so it acts as the logged-in user:

test('order created via API appears in the UI', async ({ page }) => {
  await page.goto('https://your-app.example.com/login');
  // ...log in through the UI or reuse a saved session...

  const response = await page.request.post('/api/orders', { data: { product: 'Backpack', qty: 1 } });
  await expect(response).toBeOK();
  const order = await response.json();

  await page.goto('/orders');
  await expect(page.getByText('Order #' + order.id)).toBeVisible();
});

This is faster and more reliable than creating the order by clicking through five screens. Organizing API calls in classes is covered in API automation with Page Object Model.


📝 Assignment

Use https://jsonplaceholder.typicode.com:

  1. Create an api project with baseURL and a *.api.spec.ts file.
  2. GET /users/1 and assert the user has id: 1 and a string email.
  3. GET /comments with params: { postId: 1 } and assert every comment has postId 1.
  4. POST a new todo to /todos and assert status 201 and the returned title.
  5. PATCH /todos/1 to completed: true and assert the response.
  6. DELETE /todos/1 and assert status 200.
  7. Bonus: write an interface for Todo and a generic helper getJson<T>(request, url) that returns typed data.

❓ FAQ

🤔 Can Playwright be used for API testing?

Yes. The built-in request fixture (APIRequestContext) sends GET, POST, PUT, PATCH and DELETE requests, and you assert responses with the same expect as UI tests.

🤔 How do I send a POST request in Playwright?

await request.post('/posts', { data: { title: 'x' } }). Passing an object as data sends it as JSON.

🤔 How do I add an authorization header in Playwright API tests?

Set extraHTTPHeaders: { Authorization: 'Bearer ' + token } in the config or in request.newContext(), or pass headers on a single request.

🤔 Do Playwright API tests need a browser?

No. The request fixture sends HTTP requests directly, so API tests run without launching a browser.

🤔 Is Playwright a replacement for Postman?

For automated API tests in CI, yes: tests live in code, run with your UI suite and share one report. Postman remains handy for manual exploration.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in