Unlock the full potential of Playwright, Microsoft's cutting-edge browser automation framework, with this comprehensive guide to modern web testing. Designed for developers and QA professionals, this repository provides practical examples, best practices, and real-world automation patterns.
From setting up your first test environment to mastering advanced techniques like AI-powered test generation, visual regression testing, and CI/CD integration, each chapter builds upon the last to equip you with the skills needed for professional test automation.
Written by Brian McCarthy
- Languages & Technologies
- Methodologies & Patterns
- Project Functions & Features
- File Structure
- Project Overview
- Code Methodology Summary
- REST API Automation Guide
- Examples & Tutorials
- Tips & Best Practices
| Language | Percentage | Usage |
|---|---|---|
| TypeScript | 96.8% | Core automation framework, test specs, page objects, fixtures |
| Batchfile | 2.7% | Build scripts and automation runners |
| Other | 0.5% | Configuration and documentation |
- @playwright/test (v1.55.0) - Modern browser automation framework
- TypeScript (v5.9.2) - Type-safe JavaScript for robust automation code
- @faker-js/faker (v10.0.0) - Test data generation
- dotenv (v17.2.2) - Environment configuration management
- ts-node (v10.9.2) - TypeScript execution for Node.js
- @types/node (v24.3.1) - Node.js type definitions
The Page Object Model pattern encapsulates page-specific interactions into reusable classes, improving maintainability and reducing code duplication.
Key Benefits:
- Centralized element locators
- Abstracted user interactions
- Easy maintenance when UI changes
- Better code organization
Example Structure:
pages/
├── base.page.ts # Base class with common functionality
├── product.page.ts # Product-specific page interactions
└── login.page.ts # Authentication page interactions
Custom Playwright fixtures provide pre-authenticated browser contexts, enabling efficient test execution with proper authentication state.
Key Benefits:
- Reusable authentication logic
- Dependency injection pattern
- Cleaner test code
- Performance optimization
Dedicated setup tests manage authentication and prepare test data before running actual test scenarios.
Using @faker-js/faker for generating realistic test data dynamically.
Environment variables and configuration files control test behavior across different environments.
| Function | Location | Purpose |
|---|---|---|
gotoHome() |
BasePage | Navigate to application homepage |
selectCategory(category: string) |
BasePage | Select product category from navigation |
login(email: string, password: string) |
LoginPage | Authenticate user with credentials |
addToCartButton.click() |
ProductPage | Add product to shopping cart |
clearStorage(page: Page) |
BasePage | Clear local/session storage and cookies |
goto(productId?: string) |
ProductPage | Navigate to specific product page |
clickProductByIndex(index: number) |
ProductPage | Click product by position in list |
- Browser Automation: Full control over Chromium, Firefox, and WebKit browsers
- Parallel Test Execution: Run tests concurrently for faster feedback
- Authentication Management: Save and reuse authenticated sessions
- Visual & Video Recording: Capture screenshots and videos on test failures
- Trace Recording: Debug failed tests with detailed execution traces
- HTML Reporting: Comprehensive test reports with results visualization
- CI/CD Integration: Optimized for GitHub Actions and other CI platforms
- Custom Test ID Attributes: Target elements using
data-testattributes
Playwright-Automation-TypeScript/
├── Chapter01-16/ # Individual chapter projects
│
├── Chapter16/
│ └── ecom-test-project/ # Complete e-commerce test automation project
│ ├── tests/
│ │ ├── auth.setup.ts # Authentication setup test
│ │ └── product.spec.ts # Product functionality tests
│ │
│ ├── pages/
│ │ ├── base.page.ts # Base page class with common methods
│ │ ├── product.page.ts # Product page object model
│ │ └── login.page.ts # Login page object model
│ │
│ ├── fixtures/
│ │ └── adminAuth.fixture.ts # Custom authentication fixture
│ │
│ ├── data/
│ │ └── (test data factories) # Dynamic test data generation
│ │
│ ├── playwright.config.ts # Playwright configuration
│ ├── package.json # Project dependencies
│ ├── .env # Environment variables (not in repo)
│ └── .auth/ # Stored authentication states
│ ├── customer1StorageState.json
│ └── adminStorageState.json
│
└── README.md # This file
Contains Playwright test files (.spec.ts) that define test scenarios and .setup.ts files for test fixtures.
Encapsulates page-specific element locators and interactions:
- BasePage: Common navigation and utility methods used by all pages
- ProductPage: Product browsing and cart operations
- LoginPage: Authentication workflows
Custom Playwright fixtures that extend the base test object with reusable, pre-configured browser contexts and pages.
Test data factories and utilities for generating realistic test data using libraries like faker.
This repository is a comprehensive tutorial on professional Playwright automation testing. The main project is located in Chapter16/ecom-test-project/, which demonstrates a complete e-commerce test automation suite.
The e-commerce test project automates testing for:
- User authentication (login/logout)
- Product browsing and category selection
- Shopping cart operations
- Product information display
- Navigation functionality
Tests are configured to run against: https://practicesoftwaretesting.com
How It Works:
// pages/base.page.ts - Base class with shared functionality
export class BasePage {
readonly page: Page;
readonly navMenu: Locator;
readonly navMenuHome: Locator;
constructor(page: Page) {
this.page = page;
this.navMenu = page.getByTestId("nav-menu");
this.navMenuHome = page.getByTestId("nav-home");
}
async gotoHome() {
await this.page.goto("/");
}
async selectCategory(category: string) {
await this.navMenuCategories.click();
await this.navCategoryList.getByText(`${category}`).click();
}
}Benefits:
- All element locators in one place
- Easy to update when UI changes
- Reusable methods across tests
- Clear separation of concerns
How It Works:
// fixtures/adminAuth.fixture.ts - Pre-authenticated page fixture
export const test = base.extend<{ adminAuthPage: Page }>({
adminAuthPage: async ({ browser }, use) => {
const storageStatePath = ".auth/adminStorageState.json";
// Login and save authentication state
const setupContext = await browser.newContext();
const setupPage = await setupContext.newPage();
const loginPage = new LoginPage(setupPage);
await loginPage.goto();
await loginPage.login(
process.env.ADMIN_EMAIL!,
process.env.ADMIN_PASSWORD!
);
await setupContext.storageState({ path: storageStatePath });
await setupContext.close();
// Provide authenticated page to tests
const context = await browser.newContext({
storageState: storageStatePath,
});
const page = await context.newPage();
await use(page);
await context.close();
},
});Benefits:
- Avoid repeated login in every test
- Faster test execution
- Clean separation of setup and test logic
- Reusable across multiple tests
How It Works:
// tests/auth.setup.ts - Create and save authentication state
setup("Create Customer 1 Authentication", async ({ page, context }) => {
const email = process.env.CUSTOMER_1_EMAIL || "";
const password = process.env.CUSTOMER_1_PASSWORD || "";
const customer1AuthFile = ".auth/customer1StorageState.json";
const loginPage = new LoginPage(page);
await loginPage.gotoHome();
await loginPage.navMenuSignIn.click();
await loginPage.login(email, password);
// Verify and save authentication
await expect(loginPage.navMenu).toContainText("Jane Doe");
await context.storageState({ path: customer1AuthFile });
});How It Works:
// tests/product.spec.ts - Clean, readable test using page objects
test("Add product to cart", async ({ page }) => {
const productPage = new ProductPage(page);
// Navigate
await productPage.gotoHome();
await productPage.selectCategory("Hand Tools");
await productPage.clickProductByIndex(0);
// Verify product information
await expect(productPage.productName).toHaveText("Combination Pliers");
// Perform action
await productPage.addToCartButton.click();
// Verify success
await expect(productPage.addedToCartMessage).toHaveText(
"Product added to shopping cart."
);
// Wait for message to disappear
await expect(productPage.addedToCartMessage).not.toBeVisible({
timeout: 10000,
});
// Verify cart updated
await expect(productPage.navCartQuantity).toHaveText("1");
});Benefits:
- Tests read like business requirements
- No implementation details visible
- Easy to understand what's being tested
- Simple to maintain and modify
How It Works:
// playwright.config.ts - Centralized configuration
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
use: {
trace: "on",
baseURL: process.env.BASE_URL || "https://practicesoftwaretesting.com",
testIdAttribute: "data-test",
headless: true,
video: "retain-on-failure",
screenshot: "only-on-failure",
},
projects: [
{
name: "setup",
testMatch: /.*\.setup\.ts/,
},
{
name: "customer1-chromium",
dependencies: ["setup"],
use: {
...devices["Desktop Chrome"],
storageState: ".auth/customer1StorageState.json",
},
},
],
});Benefits:
- Single source of truth for configuration
- Easy to adjust for different environments
- Supports multiple browser/device combinations
- CI/CD optimizations built-in
While Playwright is primarily a browser automation tool, it also supports HTTP API testing through the APIRequestContext. This guide shows how to automate REST API testing.
import { test, expect } from "@playwright/test";
test("API: Verify product endpoint", async ({ request }) => {
// GET request
const response = await request.get("/products");
// Verify response status
expect(response.status()).toBe(200);
// Parse and verify JSON response
const data = await response.json();
expect(data).toHaveProperty("products");
expect(Array.isArray(data.products)).toBeTruthy();
});test("API: Create new product", async ({ request }) => {
const response = await request.post("/products", {
data: {
name: "New Product",
description: "Product description",
price: 99.99,
category: "Electronics",
},
});
expect(response.status()).toBe(201);
const createdProduct = await response.json();
expect(createdProduct).toHaveProperty("id");
expect(createdProduct.name).toBe("New Product");
});test("API: Authenticated endpoint", async ({ request }) => {
const response = await request.get("/api/user/profile", {
headers: {
"Authorization": `Bearer ${process.env.API_TOKEN}`,
"Content-Type": "application/json",
},
});
expect(response.status()).toBe(200);
const profile = await response.json();
expect(profile.id).toBeDefined();
});test("API: Custom headers", async ({ request }) => {
const response = await request.get("/api/data", {
headers: {
"X-Custom-Header": "CustomValue",
"Accept-Language": "en-US",
},
});
// Verify response headers
expect(response.headers()["content-type"]).toContain("application/json");
});test("API: Handle error responses", async ({ request }) => {
const response = await request.get("/api/products/invalid-id");
// Don't throw on non-2xx status
expect(response.status()).toBe(404);
const error = await response.json();
expect(error.message).toContain("Not found");
});test("API + UI: Create via API, verify in UI", async ({ page, request }) => {
// Create product via API
const apiResponse = await request.post("/products", {
data: {
name: "Test Product",
price: 49.99,
},
});
const newProduct = await apiResponse.json();
// Verify in UI
await page.goto(`/products/${newProduct.id}`);
await expect(page.getByText("Test Product")).toBeVisible();
await expect(page.getByText("$49.99")).toBeVisible();
});// fixtures/api.fixture.ts
import { test as base } from "@playwright/test";
export const test = base.extend<{ apiRequest: any }>({
apiRequest: async ({ request }, use) => {
const baseHeaders = {
"Authorization": `Bearer ${process.env.API_TOKEN}`,
"Content-Type": "application/json",
};
const api = {
get: (url: string) => request.get(url, { headers: baseHeaders }),
post: (url: string, data: any) =>
request.post(url, { data, headers: baseHeaders }),
put: (url: string, data: any) =>
request.put(url, { data, headers: baseHeaders }),
delete: (url: string) => request.delete(url, { headers: baseHeaders }),
};
await use(api);
},
});
export { expect } from "@playwright/test";test("API: Retry on failure", async ({ request }) => {
let response;
let retries = 3;
while (retries > 0) {
response = await request.get("/api/data");
if (response.status() === 200) {
break;
}
retries--;
await new Promise(resolve => setTimeout(resolve, 1000));
}
expect(response?.status()).toBe(200);
});Step 1: Install Dependencies
cd Chapter16/ecom-test-project
npm install
npx playwright installStep 2: Create Environment File
# .env file
CUSTOMER_1_EMAIL=customer@example.com
CUSTOMER_1_PASSWORD=password123
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=admin123
BASE_URL=https://practicesoftwaretesting.comStep 3: Run Tests
# Run all tests
npx playwright test
# Run in headed mode (see browser)
npx playwright test --headed
# Run specific test file
npx playwright test product.spec.ts
# Run tests matching pattern
npx playwright test --grep "Add product"
# View detailed HTML report
npx playwright show-report// pages/cart.page.ts
import { Locator, Page } from "@playwright/test";
import { BasePage } from "./base.page";
export class CartPage extends BasePage {
readonly cartItems: Locator;
readonly cartTotal: Locator;
readonly checkoutButton: Locator;
constructor(page: Page) {
super(page);
this.cartItems = page.locator(".cart-item");
this.cartTotal = page.getByTestId("cart-total");
this.checkoutButton = page.getByTestId("checkout");
}
async goto() {
await this.page.goto("/cart");
}
async getItemCount() {
return await this.cartItems.count();
}
async getTotalPrice() {
const totalText = await this.cartTotal.textContent();
return parseFloat(totalText?.replace(/[^\d.]/g, "") || "0");
}
async proceedToCheckout() {
await this.checkoutButton.click();
}
}// tests/cart.spec.ts
import { test, expect } from "@playwright/test";
import { CartPage } from "../pages/cart.page";
import { ProductPage } from "../pages/product.page";
test("Complete shopping flow", async ({ page }) => {
// Initialize page objects
const productPage = new ProductPage(page);
const cartPage = new CartPage(page);
// Browse products
await productPage.gotoHome();
await productPage.selectCategory("Hand Tools");
await productPage.clickProductByIndex(0);
await productPage.addToCartButton.click();
// View cart
await cartPage.goto();
expect(await cartPage.getItemCount()).toBe(1);
// Verify total
const total = await cartPage.getTotalPrice();
expect(total).toBeGreaterThan(0);
// Proceed to checkout
await cartPage.proceedToCheckout();
await expect(page).toHaveURL(/.*checkout/);
});// data/productFactory.ts
import { faker } from "@faker-js/faker";
export const createProductData = () => ({
name: faker.commerce.productName(),
description: faker.commerce.productDescription(),
price: parseFloat(faker.commerce.price()),
category: faker.commerce.department(),
inStock: faker.datatype.boolean(),
});
// Usage in test
import { createProductData } from "../data/productFactory";
test("Create product with fake data", async ({ request }) => {
const productData = createProductData();
const response = await request.post("/products", {
data: productData,
});
expect(response.status()).toBe(201);
const created = await response.json();
expect(created.name).toBe(productData.name);
});// ✅ GOOD - Resilient to UI changes
const element = page.getByTestId("product-name");
// ❌ AVOID - Breaks with CSS changes
const element = page.locator("div.product > h1.title");// ✅ GOOD - Wait for specific element
await expect(page.getByText("Success")).toBeVisible();
// ❌ AVOID - Fixed waits
await page.waitForTimeout(3000);// ✅ GOOD - User-centric selectors
await page.getByLabel("Email").fill("test@example.com");
await page.getByRole("button", { name: "Login" }).click();
// ❌ AVOID - Implementation details
await page.locator("input#email_field_23").fill("test@example.com");
await page.locator("button.btn-primary.large").click();// Use proper wait strategies
await page.waitForLoadState("networkidle");
await page.waitForSelector(".product-card", { state: "visible" });
await expect(page.locator(".product-card")).toHaveCount(10);// ✅ GOOD - Each test stands alone
test("User can add product to cart", async ({ page }) => {
await page.goto("/");
// Complete flow in single test
});
// ❌ AVOID - Dependencies between tests
test.describe.serial("Workflow", () => {
test("First", () => { /* setup */ });
test("Second", () => { /* depends on First */ });
});// ✅ GOOD
test("Verify user receives error message when submitting form with invalid email", () => {});
// ❌ AVOID
test("Test form", () => {});// Tests run in parallel by default in Playwright
// Control with:
test.describe.serial("Sequential tests", () => {
test("First", () => {});
test("Second", () => {});
});
test.describe("Parallel tests", () => {
test("First", () => {});
test("Second", () => {});
});# Run with inspector
npx playwright test --debug
# Run single test with headed mode
npx playwright test --headed product.spec.ts
# Generate trace for analysis
npx playwright test --trace on
# View trace
npx playwright show-trace trace.ziptest("Operation with extended timeout", async ({ page }) => {
// Custom timeout for specific action
await expect(page.getByText("Loading...")).not.toBeVisible({
timeout: 30000, // 30 seconds
});
// Or set global timeout in config
// expect.setDefaultTimeout(15000);
});import { devices } from "@playwright/test";
export default defineConfig({
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "mobile", use: { ...devices["iPhone 12"] } },
{ name: "tablet", use: { ...devices["iPad Pro"] } },
],
});test("Product page layout", async ({ page }) => {
await page.goto("/products/123");
// Take screenshot for comparison
await expect(page).toHaveScreenshot("product-page.png");
});
// Run with:
// npx playwright test --update-snapshotsconst baseURL = process.env.ENV === "staging"
? "https://staging.example.com"
: "https://production.example.com";
test("Cross-environment test", async ({ page }) => {
await page.goto(baseURL);
// Test runs on configured URL
});# Install dependencies
npm install
npx playwright install
# Run all tests
npx playwright test
# Run in headed mode (see browser)
npx playwright test --headed
# Run specific file
npx playwright test product.spec.ts
# Run tests matching pattern
npx playwright test --grep "@smoke"
# Run single test
npx playwright test -g "Add product to cart"
# Debug mode
npx playwright test --debug
# View test report
npx playwright show-reportThe playwright.config.ts is pre-configured for CI environments:
- Retries are enabled in CI
- Single worker in CI to avoid conflicts
- Videos captured on failure
- Screenshots captured on failure
- HTML reports generated
- Use Page Object Model for maintainability
- Leverage Fixtures for setup and reusable context
- Keep Tests Independent - no test interdependencies
- Use Meaningful Assertions - verify actual outcomes
- Handle Waits Properly - use Playwright's auto-waiting
- Test from User Perspective - use semantic selectors
- Maintain Test Data - use factories for realistic data
- Document Test Purpose - clear test descriptions
- Monitor Test Health - track flaky tests
- Iterate and Improve - refactor tests regularly
- Chapter 1-15 - Individual learning modules
- Chapter 16 - E-commerce Project - Complete example project
- Playwright Documentation
- Playwright API Reference
- Playwright Best Practices
- Playwright Debugging
- Practice Testing Website
ISC
Written by Brian McCarthy
For questions, improvements, or contributions, please refer to the individual chapter directories for specific implementation details.