Playwright Testing Guide

📋 Standards ← Docs v1.40+
1

Getting Started

Setup

Playwright is a Node.js library for browser automation. It supports Chromium, Firefox, and WebKit with a single API.

Installation

  1. Scaffold a new Playwright project (installs everything interactively)
  2. Or add to an existing project manually
  3. Install the browser binaries
bash
# Option A – scaffold new project
npm init playwright@latest

# Option B – add to existing project
npm install @playwright/test

# Install browser binaries (Chromium, Firefox, WebKit)
npx playwright install

Project Structure

text
my-project/
├── tests/                    # Test spec files (.spec.js / .spec.ts)
│   ├── ClientAppPO.spec.js
│   ├── APITest.spec.js
│   └── VisualTest.spec.js
├── pageobjects/              # Page Object classes (JS)
│   ├── LoginPage.js
│   ├── DashBoardPage.js
│   ├── CheckoutPage.js
│   ├── OrderHistoryPage.js
│   └── POManager.js
├── pageobjects_ts/           # Page Object classes (TypeScript)
│   ├── LoginPage.ts
│   ├── DashBoardPage.ts
│   └── POManager.ts
├── utils/                    # Helpers & test data
│   ├── APIUtils.js
│   └── placeorderTestData.json
├── features/                 # Cucumber BDD
│   ├── Ecommerce.feature
│   ├── step_definitions/
│   └── support/hooks.js
├── playwright.config.js      # Basic config
├── playwright.config1.js     # Advanced multi-project config
└── package.json
💡 Tip: Run npx playwright test --ui to open the interactive UI mode — great for debugging and exploring tests visually.
2

Playwright Config

Config

Playwright is configured via playwright.config.js (or .ts). You can have multiple config files for different environments or browser setups.

playwright.config.js — Basic

javascript
const config = {
  testDir: "./tests",
  timeout: 30 * 1000,
  expect: { timeout: 5000 },
  reporter: "html",
  use: {
    browserName: "chromium",
    headless: false,
    screenshot: "on",
    trace: "on",
  },
};

module.exports = config;

playwright.config1.js — Advanced (Multi-Project)

Use projects to run the same tests across multiple browsers or device profiles in one command.

javascript
const { devices } = require("@playwright/test");

const config = {
  testDir: "./tests",
  retries: 1,
  workers: 3,
  timeout: 30 * 1000,
  projects: [
    {
      name: "safari-execution",
      use: {
        browserName: "webkit",
        headless: false,
        screenshot: "on",
        trace: "retain-on-failure",
        ...devices["iPhone X"],
      },
    },
    {
      name: "chrome-execution",
      use: {
        browserName: "chromium",
        headless: false,
        screenshot: "on",
        video: "retain-on-failure",
        ignoreHttpsErrors: true,
        permissions: ["geolocation"],
        trace: "retain-on-failure",
        viewport: { width: 720, height: 720 },
      },
    },
  ],
};

module.exports = config;

Running a Specific Project

bash
npx playwright test --config playwright.config1.js --project chrome-execution
npx playwright test --config playwright.config1.js --project safari-execution

trace options

  • "on" — always record
  • "off" — never record
  • "retain-on-failure" — only keep on fail
  • "on-first-retry" — record on retry

screenshot options

  • "on" — always capture
  • "off" — never capture
  • "only-on-failure" — on fail only
3

Locators & Selectors

Core

Playwright provides multiple ways to locate elements. getBy* locators are preferred as they are more resilient and readable.

CSS Selectors

javascript
page.locator("input#login")                              // by ID
page.locator("input#userEmail")                          // input by ID
page.locator(".card-body")                               // by class
page.locator(".card-body b")                             // nested element
page.locator("[type='password']")                        // by attribute
page.locator("[placeholder*='Country']")                 // attribute contains
page.locator("[style*='block']")                         // style contains
page.locator("button[type='button']")                    // tag + attribute
page.locator("button[routerlink*='/dashboard/myorders']") // partial attribute
page.locator("table th[scope='row']")                    // complex CSS

getBy Selectors (Preferred)

🎯 Best practice: Prefer getByRole, getByLabel, and getByText — they reflect how users interact with the page and are more resilient to markup changes.
javascript
page.getByRole("button", { name: "Hide" })
page.getByRole("button", { name: "Confirm" })
page.getByRole("button", { name: "Mouse Hover" })
page.getByRole("textbox", { name: "Hide/Show Example" })
page.getByText("2027")
page.getByLabel("Email address")
page.getByPlaceholder("Search products")
page.getByTestId("submit-btn")                           // data-testid attribute

XPath

javascript
page.locator("//abbr[text()=15]")                        // xpath by exact text
page.locator("//input[@id='userEmail']")                 // xpath by attribute
page.locator("//button[contains(text(),'Login')]")       // xpath contains text

Chaining & Filtering

javascript
// Chained locators
page.locator(".form__cc").locator(".input.txt")

// Child element
page.locator(".ta-results button")

// nth element (0-indexed)
const products = page.locator(".card-body");
products.nth(0)                                          // first product
products.nth(i).locator("b")                             // nth product's title
products.nth(i).locator("button:last-child")             // nth product's last button

// first / last
page.locator("table th[scope='row']").last()
page.locator(".card-body").first()

Frames

javascript
// Access elements inside an iframe
const framesPage = page.frameLocator("#courses-iframe");
await framesPage.locator("li a[href*='lifetime-access']:visible").click();
await framesPage.locator(".course-title").textContent();
4

Actions & Interactions

Actions

All Playwright actions are async and return Promises. Always await them.

javascript
// Navigation
await page.goto("https://example.com");
await page.goBack();
await page.reload();

// Typing
await locator.fill("text");                              // clear + type
await locator.pressSequentially("Philippines", { delay: 150 }); // type char by char
await locator.clear();

// Mouse
await locator.click();
await locator.dblclick();
await locator.hover();
await locator.dragTo(targetLocator);

// Keyboard
await page.keyboard.press("Enter");
await page.keyboard.press("Tab");

// Select
await page.selectOption("select#country", "India");

// Checkboxes / Radio
await locator.check();
await locator.uncheck();

// Screenshots
await page.screenshot({ path: "screenshot.png" });
await locator.screenshot({ path: "partial.png" });

// Waiting
await page.waitForLoadState("networkidle");
await page.waitForLoadState("domcontentloaded");
await locator.waitFor();
await locator.first().waitFor();
await page.locator("table th[scope='row']").last().waitFor();
await page.waitForTimeout(1000);                         // avoid if possible

// Get values
const text = await locator.textContent();
const value = await locator.inputValue();
const count = await locator.count();
5

Assertions

Assertions

Playwright's expect assertions are auto-retrying — they poll until the condition is met or the timeout expires.

javascript
import { expect } from "@playwright/test";

// Page assertions
await expect(page).toHaveTitle("Google");
await expect(page).toHaveURL("https://example.com/dashboard");

// Visibility
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();

// Text content
await expect(locator).toHaveText("Thankyou for the order.");
await expect(locator).toContainText("Incorrect");
await expect(locator).toHaveText(/regex pattern/);

// Input values
await expect(locator).toHaveValue("2027-06-15");
await expect(locator).toHaveAttribute("type", "email");

// Count
await expect(locator).toHaveCount(5);

// Non-async (plain Jest-style)
expect(bool).toBeTruthy();
expect(value).toBe("expected");
expect(array).toContain("item");

// Visual snapshot
expect(await page.screenshot()).toMatchSnapshot("landing.png");
expect(await locator.screenshot()).toMatchSnapshot("element.png");
💡 Soft assertions: Use expect.soft(locator) to continue the test even when an assertion fails — all failures are reported at the end.
6

Page Object Model (POM)

Architecture

The Page Object Model encapsulates page-specific selectors and actions into reusable classes. Tests become cleaner and maintenance is centralized — change a selector once, fix all tests.

LoginPage.js

javascript
class LoginPage {
  constructor(page) {
    this.page = page;
    this.signInButton = page.locator("input#login");
    this.userName     = page.locator("input#userEmail");
    this.password     = page.locator("input#userPassword");
    this.loginToast   = page.locator("div#toast-container");
  }

  async goTo() {
    await this.page.goto("https://rahulshettyacademy.com/client/#/auth/login");
  }

  async validLogin(username, password) {
    await this.userName.fill(username);
    await this.password.fill(password);
    await this.signInButton.click();
    await this.page.waitForLoadState("networkidle");
  }

  async getToastMessage() {
    await this.loginToast.first().waitFor();
    return await this.loginToast.textContent();
  }
}

module.exports = LoginPage;

LoginPage.ts — TypeScript Version

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

export default class LoginPage {
  page: Page;
  signInButton: Locator;
  userName: Locator;
  password: Locator;
  loginToast: Locator;

  constructor(page: Page) {
    this.page        = page;
    this.signInButton = page.locator("input#login");
    this.userName    = page.locator("input#userEmail");
    this.password    = page.locator("input#userPassword");
    this.loginToast  = page.locator("div#toast-container");
  }

  async goTo(): Promise<void> {
    await this.page.goto("https://rahulshettyacademy.com/client/#/auth/login");
  }

  async validLogin(username: string, password: string): Promise<void> {
    await this.userName.fill(username);
    await this.password.fill(password);
    await this.signInButton.click();
    await this.page.waitForLoadState("networkidle");
  }

  async getToastMessage(): Promise<string | null> {
    await this.loginToast.first().waitFor();
    return await this.loginToast.textContent();
  }
}

DashBoardPage.js

javascript
class DashBoardPage {
  constructor(page) {
    this.page          = page;
    this.searchInput   = page.locator("input[type='search']");
    this.productTitles = page.locator(".card-body b");
    this.products      = page.locator(".card-body");
    this.cart          = page.locator("[routerlink*='cart']");
  }

  async searchProductAddCart(productName) {
    const allProducts = this.page.locator(".card-body");
    const count = await allProducts.count();

    for (let i = 0; i < count; i++) {
      const title = await allProducts.nth(i).locator("b").textContent();
      if (title === productName) {
        await allProducts.nth(i).locator("button:last-child").click();
        break;
      }
    }
  }

  async navigateToCart() {
    await this.cart.click();
    await this.page.waitForLoadState("networkidle");
  }
}

module.exports = DashBoardPage;
7

POManager

Architecture

The Page Object Manager is a factory that instantiates all page objects once and provides them to tests. This avoids creating multiple instances and keeps tests clean.

POManager.js

javascript
const LoginPage       = require("./LoginPage");
const DashBoardPage   = require("./DashBoardPage");
const CheckoutPage    = require("./CheckoutPage");
const OrderHistoryPage = require("./OrderHistoryPage");

class POManager {
  constructor(page) {
    this.page             = page;
    this.loginPage        = new LoginPage(this.page);
    this.dashboardPage    = new DashBoardPage(this.page);
    this.checkoutPage     = new CheckoutPage(this.page);
    this.orderHistoryPage = new OrderHistoryPage(this.page);
  }

  getLoginPage()        { return this.loginPage; }
  getDashboardPage()    { return this.dashboardPage; }
  getCheckoutPage()     { return this.checkoutPage; }
  getOrderHistoryPage() { return this.orderHistoryPage; }
}

module.exports = POManager;

Usage in a Test

javascript
const { test, expect } = require("@playwright/test");
const POManager = require("../pageobjects/POManager");
const data = require("../utils/placeorderTestData.json");

test("Place an order", async ({ page }) => {
  const poManager   = new POManager(page);
  const loginPage   = poManager.getLoginPage();
  const dashPage    = poManager.getDashboardPage();
  const checkoutPage = poManager.getCheckoutPage();
  const ordersPage  = poManager.getOrderHistoryPage();

  await loginPage.goTo();
  await loginPage.validLogin(data[0].userName, data[0].password);

  await dashPage.searchProductAddCart(data[0].productName);
  await dashPage.navigateToCart();

  await checkoutPage.searchCountryAndSelect("India", "India");
  await checkoutPage.placeOrder();

  await expect(checkoutPage.getOrderConfirmation()).toContainText("Thankyou");

  const orderId = await checkoutPage.getOrderId();
  await ordersPage.verifyOrderById(orderId);
});
8

API Testing

API

Playwright has a built-in request context for making HTTP calls — perfect for seeding test data or bypassing UI login flows.

APIUtils.js

javascript
export default class APIUtils {
  constructor(apiContext, loginPayload) {
    this.apiContext   = apiContext;
    this.loginPayload = loginPayload;
  }

  async getToken() {
    const loginResponse = await this.apiContext.post(
      "https://rahulshettyacademy.com/api/ecom/auth/login",
      { data: this.loginPayload }
    );
    const loginResponseJson = await loginResponse.json();
    return loginResponseJson.token;
  }

  async createOrder(orderPayload) {
    let response = {};
    response.token = await this.getToken();

    const orderResponse = await this.apiContext.post(
      "https://rahulshettyacademy.com/api/ecom/order/create-order",
      {
        data: orderPayload,
        headers: {
          Authorization: response.token,
          "Content-Type": "application/json",
        },
      }
    );
    const orderResponseJson = await orderResponse.json();
    response.orderId = orderResponseJson.orders[0];
    return response;
  }
}

Bypass UI Login — Inject Token into localStorage

Use the API to get an auth token, then inject it directly into the browser's localStorage. This skips the login UI entirely, making tests faster and more reliable.

javascript
import { test, expect, request } from "@playwright/test";
import APIUtils from "../utils/APIUtils";

const loginPayload = {
  userEmail: "user@example.com",
  userPassword: "Password123!",
};
const orderPayload = {
  orders: [{ country: "Cuba", productOrderedId: "abc123" }],
};

let response;

test.beforeAll(async () => {
  const apiContext = await request.newContext();
  const apiUtils   = new APIUtils(apiContext, loginPayload);
  response         = await apiUtils.createOrder(orderPayload);
});

test("@API Place an Order", async ({ page }) => {
  // Inject token before page loads
  await page.addInitScript((value) => {
    window.localStorage.setItem("token", value);
  }, response.token);

  await page.goto("https://rahulshettyacademy.com/client");
  // Page loads already authenticated — no login UI needed!

  // Navigate directly to order history and verify
  await page.locator("button[routerlink*='/dashboard/myorders']").click();
  await page.locator("tbody").waitFor();

  const rows = page.locator("tbody tr");
  // find the row matching our orderId
  for (let i = 0; i < await rows.count(); i++) {
    const rowText = await rows.nth(i).textContent();
    if (rowText.includes(response.orderId)) {
      await rows.nth(i).locator("button").first().click();
      break;
    }
  }
  await expect(page.locator(".col-text")).toContainText(response.orderId);
});
9

Network Interception & Mocking

Network

Intercept, block, or mock any network request. Useful for speeding up tests (block images/CSS) or testing error states without a real backend.

javascript
// Block images to speed up tests
await page.route("**/*.{jpg,png,jpeg,gif,svg}", async (route) => route.abort());

// Block CSS
await page.route("**/*.css", async (route) => route.abort());

// Mock an API response entirely
const fakePayload = { data: [], message: "No Orders" };
await page.route("https://api.example.com/orders/*", async (route) => {
  route.fulfill({
    status: 200,
    contentType: "application/json",
    body: JSON.stringify(fakePayload),
  });
});

// Intercept, modify, then forward the real response
await page.route("https://api.example.com/orders/*", async (route) => {
  const response = await page.request.fetch(route.request());
  const body     = await response.json();
  body.data      = [];                                   // mutate the response
  route.fulfill({ response, body: JSON.stringify(body) });
});

// Listen to all requests / responses
page.on("request",  (req)  => console.log(">>", req.url()));
page.on("response", (res)  => console.log("<<", res.status(), res.url()));

// Wait for a specific response before continuing
const [response] = await Promise.all([
  page.waitForResponse("https://api.example.com/orders/*"),
  page.locator("#load-orders").click(),
]);
const data = await response.json();
10

Hooks & Test Lifecycle

Lifecycle

Hooks let you run setup and teardown code at different points in the test lifecycle.

javascript
import { test, expect } from "@playwright/test";

test.beforeAll(async ({ browser }) => {
  // Runs once before all tests in the file
  // Good for: API setup, creating shared auth state
  console.log("Suite starting...");
});

test.beforeEach(async ({ page }) => {
  // Runs before each individual test
  // Good for: navigating to start URL, resetting state
  await page.goto("https://example.com");
});

test.afterEach(async ({ page }, testInfo) => {
  // Runs after each test
  // Good for: screenshots on failure, cleanup
  if (testInfo.status !== testInfo.expectedStatus) {
    await page.screenshot({ path: `failure-${testInfo.title}.png` });
  }
});

test.afterAll(async () => {
  // Runs once after all tests
  // Good for: closing connections, final cleanup
});

Session Storage / State Persistence

Log in once, save the browser storage state to a file, then reuse it across all tests — no repeated login UI flows.

javascript
let webContext;

test.beforeAll(async ({ browser }) => {
  // Create a fresh context and log in
  const context = await browser.newContext();
  const page    = await context.newPage();

  await page.goto("https://example.com/login");
  await page.locator("#email").fill("user@example.com");
  await page.locator("#password").fill("Password123!");
  await page.locator("button[type='submit']").click();
  await page.waitForLoadState("networkidle");

  // Save cookies + localStorage to file
  await context.storageState({ path: "state.json" });

  // Create a new context that starts already authenticated
  webContext = await browser.newContext({ storageState: "state.json" });
});

test("dashboard loads", async () => {
  const page = await webContext.newPage();
  await page.goto("https://example.com/dashboard");
  // Already logged in — no login step needed!
  await expect(page.locator("h1")).toContainText("Dashboard");
});

test("profile page", async () => {
  const page = await webContext.newPage();
  await page.goto("https://example.com/profile");
  // Still authenticated from the same saved state
});
11

Custom Test Fixtures

Fixtures

Fixtures extend the base test object with custom data or objects that are automatically injected into every test that uses them.

javascript
// utils/customFixtures.js
import { test as base, expect } from "@playwright/test";

export const customTest = base.extend({
  // Static data fixture
  testDataForOrder: {
    userName:    "user@example.com",
    password:    "Password123!",
    productName: "ZARA COAT 3",
    country:     "Philippines",
  },

  // Dynamic fixture (function form — runs setup/teardown)
  authenticatedPage: async ({ page }, use) => {
    await page.goto("https://example.com/login");
    await page.locator("#email").fill("user@example.com");
    await page.locator("#password").fill("Password123!");
    await page.locator("button[type='submit']").click();
    await page.waitForLoadState("networkidle");

    await use(page);  // <-- test runs here

    // teardown (optional)
    await page.close();
  },
});

export { expect };

Using Custom Fixtures

javascript
// Import your custom test instead of the default one
import { customTest, expect } from "../utils/customFixtures";

customTest("place order with fixture data", async ({ page, testDataForOrder }) => {
  console.log(testDataForOrder.productName); // "ZARA COAT 3"

  await page.goto("https://example.com/login");
  await page.locator("#email").fill(testDataForOrder.userName);
  await page.locator("#password").fill(testDataForOrder.password);
  // ...
});

customTest("pre-authenticated test", async ({ authenticatedPage }) => {
  // authenticatedPage is already logged in
  await authenticatedPage.goto("https://example.com/dashboard");
  await expect(authenticatedPage.locator("h1")).toContainText("Dashboard");
});
12

Data-Driven Tests

Data

Loop over a JSON dataset to run the same test logic with different inputs — no code duplication.

placeorderTestData.json

json
[
  {
    "userName": "user1@example.com",
    "password": "Password123!",
    "productName": "iPhone 13 Pro",
    "country": "India"
  },
  {
    "userName": "user2@example.com",
    "password": "Password456!",
    "productName": "ZARA COAT 3",
    "country": "Philippines"
  }
]

Test File

javascript
const { test, expect } = require("@playwright/test");
const POManager = require("../pageobjects/POManager");

// Load the dataset
const dataSet = JSON.parse(
  JSON.stringify(require("../utils/placeorderTestData.json"))
);

// Generate one test per data entry
for (const data of dataSet) {
  test(`Order test - ${data.productName}`, async ({ page }) => {
    const poManager  = new POManager(page);
    const loginPage  = poManager.getLoginPage();
    const dashPage   = poManager.getDashboardPage();

    await loginPage.goTo();
    await loginPage.validLogin(data.userName, data.password);

    await dashPage.searchProductAddCart(data.productName);
    await dashPage.navigateToCart();

    // ... rest of order flow
    await expect(page.locator(".order-confirm")).toBeVisible();
  });
}
📊 This generates 2 separate tests in the Playwright report — one per dataset entry — each with its own pass/fail status and trace.
13

Tags & Grep Filtering

Filtering

Prefix test names with @TagName to create logical groups. Use --grep to run only matching tests.

Tagging Tests

javascript
test("@Web Login and place order", async ({ page }) => { /* ... */ });
test("@Web Search products",       async ({ page }) => { /* ... */ });
test("@API Create order via API",  async ({ page }) => { /* ... */ });
test("@Regression Full smoke run", async ({ page }) => { /* ... */ });

Running by Tag

bash
npx playwright test --grep @Web
npx playwright test --grep @API
npx playwright test --grep @Regression

# Exclude a tag
npx playwright test --grep-invert @API

# Multiple tags (OR)
npx playwright test --grep "@Web|@API"

package.json Scripts

json
{
  "scripts": {
    "webTests":   "npx playwright test --grep @Web",
    "APITests":   "npx playwright test --grep @API",
    "regression": "npx playwright test",
    "headed":     "npx playwright test --headed",
    "report":     "npx playwright show-report"
  }
}
14

Test Suites & Describe Blocks

Structure

Use test.describe to group related tests. Configure execution mode per group.

javascript
import { test, expect } from "@playwright/test";

test.describe("Login Tests", () => {
  // Run tests in this group one after another (share state)
  test.describe.configure({ mode: "serial" });

  test("valid login redirects to dashboard", async ({ page }) => {
    await page.goto("https://example.com/login");
    await page.locator("#email").fill("user@example.com");
    await page.locator("#password").fill("Password123!");
    await page.locator("button[type='submit']").click();
    await expect(page).toHaveURL(/dashboard/);
  });

  test("invalid login shows error", async ({ page }) => {
    await page.goto("https://example.com/login");
    await page.locator("#email").fill("bad@example.com");
    await page.locator("#password").fill("wrongpass");
    await page.locator("button[type='submit']").click();
    await expect(page.locator(".error-msg")).toContainText("Incorrect");
  });
});

test.describe("Product Tests", () => {
  // Run tests in parallel (default)
  test.describe.configure({ mode: "parallel" });

  test("search returns results", async ({ page }) => { /* ... */ });
  test("add to cart works",      async ({ page }) => { /* ... */ });
  test("remove from cart works", async ({ page }) => { /* ... */ });
});

serial mode

Tests run one at a time in order. If one fails, subsequent tests are skipped. Good for dependent flows.

parallel mode

Tests run concurrently across workers. Faster but each test must be fully independent.

15

Screenshots & Visual Testing

Visual

Playwright supports both on-demand screenshots and pixel-perfect visual regression testing via snapshots.

javascript
// Full page screenshot
await page.screenshot({ path: "screenshot.png" });
await page.screenshot({ path: "full.png", fullPage: true });

// Element screenshot
await page.locator("#hero-banner").screenshot({ path: "banner.png" });

// Screenshot with clip region
await page.screenshot({
  path: "clipped.png",
  clip: { x: 0, y: 0, width: 800, height: 400 },
});

// Visual snapshot comparison
// First run: creates the baseline image
// Subsequent runs: compares against baseline
expect(await page.screenshot()).toMatchSnapshot("landing.png");
expect(await page.locator(".product-card").screenshot())
  .toMatchSnapshot("product-card.png");

// With tolerance for minor pixel differences
expect(await page.screenshot()).toMatchSnapshot("landing.png", {
  maxDiffPixelRatio: 0.01,  // allow 1% pixel difference
});
⚠️ Snapshot baseline: Run npx playwright test --update-snapshots to regenerate baseline images after intentional UI changes.
16

Tracing

Debugging

Traces capture a full recording of the test — DOM snapshots, network requests, console logs, and screenshots — viewable in the Playwright Trace Viewer.

Configure in playwright.config.js

javascript
const config = {
  use: {
    trace: "on",                  // always record
    // trace: "retain-on-failure", // only keep when test fails
    // trace: "on-first-retry",    // record on first retry
    // trace: "off",               // disabled
  },
};

Manual Tracing in Tests

javascript
test("traced test", async ({ page, context }) => {
  await context.tracing.start({ screenshots: true, snapshots: true });

  await page.goto("https://example.com");
  // ... test steps ...

  await context.tracing.stop({ path: "trace.zip" });
});

Viewing a Trace

bash
# Open trace viewer with a local file
npx playwright show-trace trace.zip

# Or open the HTML report (includes traces)
npx playwright show-report
17

Cucumber / BDD Integration

BDD

Combine Playwright with Cucumber to write tests in plain English using the Gherkin syntax. Great for collaboration with non-technical stakeholders.

Install

bash
npm install @cucumber/cucumber

Feature File (features/Ecommerce.feature)

gherkin
Feature: Ecommerce validations

  @Regression
  Scenario: Placing the Order
    Given a login to Ecommerce application with "user@example.com" and "Password123!"
    When Add "iphone 13 pro" to Cart
    Then Verify "iphone 13 pro" is displayed in the Cart
    When Enter valid details, verify "user@example.com", and Place the Order
    Then Verify order is present in the Order History

  @Validation
  Scenario Outline: Error Validation
    Given a login to Ecommerce2 application with "<username>" and "<password>"
    Then Verify Error Message is displayed

    Examples:
      | username        | password  |
      | bad@example.com | wrongpass |

Step Definitions (features/step_definitions/steps.js)

javascript
const { Given, When, Then } = require("@cucumber/cucumber");
const { expect }            = require("@playwright/test");

Given(
  "a login to Ecommerce application with {string} and {string}",
  { timeout: 100 * 1000 },
  async function (username, password) {
    const loginPage = this.poManager.getLoginPage();
    await loginPage.goTo();
    await loginPage.validLogin(username, password);
  }
);

When(
  "Add {string} to Cart",
  { timeout: 100 * 1000 },
  async function (productName) {
    const dashboardPage = this.poManager.getDashboardPage();
    await dashboardPage.searchProductAddCart(productName);
    await dashboardPage.navigateToCart();
  }
);

Then(
  "Verify {string} is displayed in the Cart",
  { timeout: 100 * 1000 },
  async function (productName) {
    const checkoutPage = this.poManager.getCheckoutPage();
    const bool = await checkoutPage.verifyProductInCart(productName);
    expect(bool).toBeTruthy();
  }
);

Then(
  "Verify order is present in the Order History",
  { timeout: 100 * 1000 },
  async function () {
    const ordersPage = this.poManager.getOrderHistoryPage();
    await ordersPage.verifyOrderById(this.orderId);
  }
);

Hooks (features/support/hooks.js)

javascript
const { Before, After, AfterStep, Status } = require("@cucumber/cucumber");
const playwright = require("@playwright/test");
const POManager  = require("../../pageobjects/POManager");

Before(async function () {
  this.browser = await playwright.chromium.launch({ headless: false });
  this.context = await this.browser.newContext();
  this.page    = await this.context.newPage();
  this.poManager = new POManager(this.page);
});

AfterStep(async function ({ result }) {
  if (result.status === Status.FAILED) {
    await this.page.screenshot({ path: "screenshot1Cucumber.png" });
  }
});

After(async function () {
  await this.browser.close();
});

Running Cucumber Tests

bash
# Run a specific feature file
npx cucumber-js features/Ecommerce.feature --exit --format html:cucumber-report.html

# Run in parallel
npx cucumber-js features/Ecommerce.feature --parallel 2 --exit --format html:cucumber-report.html

# Run by tag with retry
npx cucumber-js --tags '@Validation' --retry 1 --exit --format html:cucumber-report.html

# Run @Regression tag
npx cucumber-js --tags '@Regression' --exit --format html:cucumber-report.html

package.json Script

json
{
  "scripts": {
    "CucumberRegression": "npx cucumber-js --tags '@Validation' --retry 1 --exit --format html:cucumber-report.html"
  }
}
18

Allure Reporting

Reporting

Allure generates rich, interactive HTML reports with test history, trends, and attachments.

Install

bash
npm install allure-playwright allure-commandline

Configure Reporter

javascript
// playwright.config.js
const config = {
  reporter: [
    ["html"],                                    // built-in HTML report
    ["allure-playwright"],                        // allure report
  ],
  // ...
};

Generate & Open Report

bash
# Run tests (generates allure-results/)
npx playwright test

# Generate the HTML report from results
npx allure generate ./allure-results --clean

# Open the report in browser
npx allure open

package.json Scripts

json
{
  "scripts": {
    "test":             "npx playwright test",
    "allure:generate":  "allure generate ./allure-results --clean",
    "allure:open":      "allure open",
    "allure:report":    "npm run allure:generate && npm run allure:open"
  }
}
19

Running Tests

CLI

All the CLI commands you need for running, debugging, and reporting.

bash
# ── Basic runs ──────────────────────────────────────────────
npx playwright test                                      # run all tests
npx playwright test tests/ClientAppPO.spec.js            # run specific file
npx playwright test tests/ClientAppPO.spec.js:42         # run test at line 42

# ── Browser options ─────────────────────────────────────────
npx playwright test --headed                             # show browser window
npx playwright test --project=chromium                   # specific browser
npx playwright test --project=firefox
npx playwright test --project=webkit

# ── Parallelism & retries ────────────────────────────────────
npx playwright test --workers=3                          # 3 parallel workers
npx playwright test --retries=2                          # retry failed tests

# ── Filtering ───────────────────────────────────────────────
npx playwright test --grep @Web                          # by tag
npx playwright test --grep-invert @API                   # exclude tag
npx playwright test -g "login"                           # by test name

# ── Debugging ───────────────────────────────────────────────
npx playwright test --debug                              # step-through debugger
npx playwright test --ui                                 # interactive UI mode
PWDEBUG=1 npx playwright test                            # inspector mode

# ── Reports ─────────────────────────────────────────────────
npx playwright show-report                               # open HTML report
npx playwright show-trace trace.zip                      # open trace viewer

# ── Snapshots ───────────────────────────────────────────────
npx playwright test --update-snapshots                   # regenerate baselines

# ── Config ──────────────────────────────────────────────────
npx playwright test --config playwright.config1.js       # custom config file
💡 Pro tip: Use npx playwright test --ui during development. The UI mode lets you run individual tests, inspect locators, and view traces all in one place.