Getting Started
SetupPlaywright is a Node.js library for browser automation. It supports Chromium, Firefox, and WebKit with a single API.
Installation
- Scaffold a new Playwright project (installs everything interactively)
- Or add to an existing project manually
- Install the browser binaries
# 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
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
npx playwright test --ui to open the interactive UI
mode — great for debugging and exploring tests visually.
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
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.
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
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
Locators & Selectors
CorePlaywright provides multiple ways to locate elements. getBy* locators are preferred as they are more resilient and readable.
CSS Selectors
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)
getByRole,
getByLabel, and getByText — they reflect
how users interact with the page and are more resilient to markup
changes.
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
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
// 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
// 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();
Actions & Interactions
Actions
All Playwright actions are async and return Promises. Always
await them.
// 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();
Assertions
Assertions
Playwright's expect assertions are auto-retrying — they
poll until the condition is met or the timeout expires.
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");
expect.soft(locator) to continue the test even when an
assertion fails — all failures are reported at the end.
Page Object Model (POM)
ArchitectureThe 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
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
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
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;
POManager
ArchitectureThe 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
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
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);
});
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
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.
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);
});
Network Interception & Mocking
NetworkIntercept, block, or mock any network request. Useful for speeding up tests (block images/CSS) or testing error states without a real backend.
// 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();
Hooks & Test Lifecycle
LifecycleHooks let you run setup and teardown code at different points in the test lifecycle.
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.
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
});
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.
// 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
// 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");
});
Data-Driven Tests
DataLoop over a JSON dataset to run the same test logic with different inputs — no code duplication.
placeorderTestData.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
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();
});
}
Tags & Grep Filtering
Filtering
Prefix test names with @TagName to create logical
groups. Use --grep to run only matching tests.
Tagging Tests
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
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
{
"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"
}
}
Test Suites & Describe Blocks
Structure
Use test.describe to group related tests. Configure
execution mode per group.
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.
Screenshots & Visual Testing
VisualPlaywright supports both on-demand screenshots and pixel-perfect visual regression testing via snapshots.
// 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
});
npx playwright test --update-snapshots to regenerate
baseline images after intentional UI changes.
Tracing
DebuggingTraces 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
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
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
# Open trace viewer with a local file
npx playwright show-trace trace.zip
# Or open the HTML report (includes traces)
npx playwright show-report
Cucumber / BDD Integration
BDDCombine Playwright with Cucumber to write tests in plain English using the Gherkin syntax. Great for collaboration with non-technical stakeholders.
Install
npm install @cucumber/cucumber
Feature File (features/Ecommerce.feature)
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)
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)
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
# 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
{
"scripts": {
"CucumberRegression": "npx cucumber-js --tags '@Validation' --retry 1 --exit --format html:cucumber-report.html"
}
}
Allure Reporting
ReportingAllure generates rich, interactive HTML reports with test history, trends, and attachments.
Install
npm install allure-playwright allure-commandline
Configure Reporter
// playwright.config.js
const config = {
reporter: [
["html"], // built-in HTML report
["allure-playwright"], // allure report
],
// ...
};
Generate & Open Report
# 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
{
"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"
}
}
Running Tests
CLIAll the CLI commands you need for running, debugging, and reporting.
# ── 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
npx playwright test --ui during development. The UI
mode lets you run individual tests, inspect locators, and view
traces all in one place.