best practices•50 min read•Updated Jan 25, 2026

Playwright Framework Best Practices

Learn to architect production-ready Playwright test automation frameworks using proven design patterns.

Tool:PlaywrightLevel:advancedDomain:QA Engineering

Introduction

Build production-grade Playwright test frameworks with TypeScript, following industry best practices. This comprehensive guide covers architecture, design patterns, configuration, and CI/CD integration.

šŸ’” What You'll Learn:
  • Modern project structure with TypeScript
  • Page Object Model with fixtures pattern
  • Custom fixtures for dependency injection
  • Configuration management for multiple environments
  • Parallel execution and test organization
  • Reporting with HTML and Allure
  • CI/CD integration (GitHub Actions, GitLab CI)
  • Best practices and anti-patterns to avoid
šŸŽÆ Framework Goals:
  • Type Safety: Full TypeScript for better IDE support
  • Scalability: Handle 100s of tests efficiently
  • Maintainability: Easy to update and extend
  • Speed: Fast execution with parallel testing
  • Reliability: Consistent results across environments

1. Project Structure & Setup

Recommended Project Structure

Architecture
Project Structure
playwright-framework/
ā”œā”€ā”€ tests/
│   ā”œā”€ā”€ auth/
│   │   ā”œā”€ā”€ login.spec.ts
│   │   └── signup.spec.ts
│   ā”œā”€ā”€ dashboard/
│   │   ā”œā”€ā”€ widgets.spec.ts
│   │   └── settings.spec.ts
│   └── checkout/
│       └── payment.spec.ts
│
ā”œā”€ā”€ pages/
│   ā”œā”€ā”€ BasePage.ts
│   ā”œā”€ā”€ LoginPage.ts
│   ā”œā”€ā”€ DashboardPage.ts
│   └── CheckoutPage.ts
│
ā”œā”€ā”€ fixtures/
│   ā”œā”€ā”€ test.ts              # Custom fixtures
│   └── pages.fixture.ts     # Page object fixtures
│
ā”œā”€ā”€ utils/
│   ā”œā”€ā”€ helpers.ts
│   ā”œā”€ā”€ api-helpers.ts
│   └── test-data.ts
│
ā”œā”€ā”€ config/
│   ā”œā”€ā”€ env.config.ts
│   └── test.config.ts
│
ā”œā”€ā”€ test-data/
│   ā”œā”€ā”€ users.json
│   └── products.json
│
ā”œā”€ā”€ setup/
│   ā”œā”€ā”€ global-setup.ts
│   ā”œā”€ā”€ global-teardown.ts
│   └── auth.setup.ts
│
ā”œā”€ā”€ playwright.config.ts     # Main config
ā”œā”€ā”€ .env.example
ā”œā”€ā”€ .gitignore
ā”œā”€ā”€ package.json
└── tsconfig.json
šŸ’” Structure Principles:
  • Tests: Organized by feature/domain
  • Pages: Page Object classes with TypeScript
  • Fixtures: Dependency injection for setup/teardown
  • Utils: Reusable helper functions
  • Config: Environment-specific configurations
  • Setup: Global setup for authentication, database, etc.

Package Setup

Setup
package.json
{
  "name": "playwright-framework",
  "version": "1.0.0",
  "scripts": {
    "test": "playwright test",
    "test:headed": "playwright test --headed",
    "test:debug": "playwright test --debug",
    "test:ui": "playwright test --ui",
    "test:chrome": "playwright test --project=chromium",
    "test:firefox": "playwright test --project=firefox",
    "test:webkit": "playwright test --project=webkit",
    "test:mobile": "playwright test --project=mobile",
    "test:api": "playwright test tests/api/",
    "test:smoke": "playwright test --grep @smoke",
    "test:regression": "playwright test --grep @regression",
    "report": "playwright show-report",
    "codegen": "playwright codegen",
    "trace": "playwright show-trace",
    "install:browsers": "playwright install --with-deps"
  },
  "devDependencies": {
    "@playwright/test": "^1.40.0",
    "@types/node": "^20.10.0",
    "dotenv": "^16.3.1",
    "typescript": "^5.3.3"
  }
}
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": ".",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "types": ["node", "@playwright/test"],
    "baseUrl": ".",
    "paths": {
      "@pages/*": ["pages/*"],
      "@fixtures/*": ["fixtures/*"],
      "@utils/*": ["utils/*"],
      "@config/*": ["config/*"]
    }
  },
  "include": [
    "tests/**/*",
    "pages/**/*",
    "fixtures/**/*",
    "utils/**/*",
    "config/**/*",
    "setup/**/*"
  ],
  "exclude": ["node_modules", "test-results", "playwright-report"]
}

2. Page Object Model with TypeScript

BasePage Implementation

Page Object
TypeScript - pages/BasePage.ts
import { Page, Locator } from '@playwright/test';

export class BasePage {
  protected page: Page;
  protected baseURL: string;

  constructor(page: Page) {
    this.page = page;
    this.baseURL = process.env.BASE_URL || 'https://example.com';
  }

  /**
   * Navigate to a relative URL
   */
  async goto(path: string = '') {
    await this.page.goto(`${this.baseURL}${path}`);
  }

  /**
   * Wait for page to be fully loaded
   */
  async waitForPageLoad() {
    await this.page.waitForLoadState('networkidle');
  }

  /**
   * Get page title
   */
  async getTitle(): Promise<string> {
    return await this.page.title();
  }

  /**
   * Get current URL
   */
  getCurrentURL(): string {
    return this.page.url();
  }

  /**
   * Take screenshot
   */
  async takeScreenshot(name: string) {
    await this.page.screenshot({ 
      path: `screenshots/${name}.png`,
      fullPage: true 
    });
  }

  /**
   * Wait for element to be visible
   */
  async waitForElement(locator: Locator, timeout: number = 30000) {
    await locator.waitFor({ state: 'visible', timeout });
  }

  /**
   * Check if element is visible
   */
  async isElementVisible(locator: Locator): Promise<boolean> {
    try {
      await locator.waitFor({ state: 'visible', timeout: 5000 });
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Click element with retry
   */
  async clickWithRetry(locator: Locator, maxAttempts: number = 3) {
    for (let i = 0; i < maxAttempts; i++) {
      try {
        await locator.click({ timeout: 5000 });
        return;
      } catch (error) {
        if (i === maxAttempts - 1) throw error;
        await this.page.waitForTimeout(1000);
      }
    }
  }

  /**
   * Fill input with clear
   */
  async fillInput(locator: Locator, text: string) {
    await locator.clear();
    await locator.fill(text);
  }

  /**
   * Get element text
   */
  async getElementText(locator: Locator): Promise<string> {
    return await locator.textContent() || '';
  }

  /**
   * Press keyboard key
   */
  async pressKey(key: string) {
    await this.page.keyboard.press(key);
  }
}

Page Object Example

Page Object
TypeScript - pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
  // Locators
  private readonly emailInput: Locator;
  private readonly passwordInput: Locator;
  private readonly loginButton: Locator;
  private readonly errorMessage: Locator;
  private readonly forgotPasswordLink: Locator;

  constructor(page: Page) {
    super(page);
    
    // Initialize locators
    this.emailInput = page.getByRole('textbox', { name: 'Email' });
    this.passwordInput = page.getByRole('textbox', { name: 'Password' });
    this.loginButton = page.getByRole('button', { name: 'Sign In' });
    this.errorMessage = page.locator('[data-testid="error-message"]');
    this.forgotPasswordLink = page.getByRole('link', { name: 'Forgot Password?' });
  }

  /**
   * Navigate to login page
   */
  async goto() {
    await super.goto('/login');
    await this.waitForPageLoad();
  }

  /**
   * Enter email
   */
  async enterEmail(email: string) {
    await this.emailInput.fill(email);
  }

  /**
   * Enter password
   */
  async enterPassword(password: string) {
    await this.passwordInput.fill(password);
  }

  /**
   * Click login button
   */
  async clickLogin() {
    await this.loginButton.click();
  }

  /**
   * Complete login flow (method chaining)
   */
  async login(email: string, password: string) {
    await this.enterEmail(email);
    await this.enterPassword(password);
    await this.clickLogin();
    // Wait for navigation
    await this.page.waitForURL('**/dashboard');
  }

  /**
   * Get error message
   */
  async getErrorMessage(): Promise<string> {
    return await this.errorMessage.textContent() || '';
  }

  /**
   * Check if error is displayed
   */
  async isErrorDisplayed(): Promise<boolean> {
    return await this.isElementVisible(this.errorMessage);
  }

  /**
   * Click forgot password
   */
  async clickForgotPassword() {
    await this.forgotPasswordLink.click();
  }

  /**
   * Verify login page is loaded
   */
  async isLoaded(): Promise<boolean> {
    return await this.isElementVisible(this.loginButton) &&
           await this.isElementVisible(this.emailInput);
  }
}
TypeScript - pages/DashboardPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export class DashboardPage extends BasePage {
  // Locators
  private readonly welcomeMessage: Locator;
  private readonly userMenu: Locator;
  private readonly settingsLink: Locator;
  private readonly logoutButton: Locator;

  constructor(page: Page) {
    super(page);
    
    this.welcomeMessage = page.locator('[data-testid="welcome-message"]');
    this.userMenu = page.getByRole('button', { name: 'User Menu' });
    this.settingsLink = page.getByRole('link', { name: 'Settings' });
    this.logoutButton = page.getByRole('button', { name: 'Logout' });
  }

  /**
   * Navigate to dashboard
   */
  async goto() {
    await super.goto('/dashboard');
    await this.waitForPageLoad();
  }

  /**
   * Get welcome message text
   */
  async getWelcomeMessage(): Promise<string> {
    return await this.welcomeMessage.textContent() || '';
  }

  /**
   * Open user menu
   */
  async openUserMenu() {
    await this.userMenu.click();
  }

  /**
   * Navigate to settings
   */
  async goToSettings() {
    await this.openUserMenu();
    await this.settingsLink.click();
  }

  /**
   * Logout
   */
  async logout() {
    await this.openUserMenu();
    await this.logoutButton.click();
  }

  /**
   * Check if dashboard is loaded
   */
  async isLoaded(): Promise<boolean> {
    return await this.isElementVisible(this.welcomeMessage);
  }
}

3. Custom Fixtures for Dependency Injection

Custom Fixtures Implementation

Fixtures
TypeScript - fixtures/test.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

// Extend basic test with custom fixtures
type MyFixtures = {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
  authenticatedPage: Page;
};

export const test = base.extend<MyFixtures>({
  // LoginPage fixture
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
    // Automatic cleanup
  },

  // DashboardPage fixture
  dashboardPage: async ({ page }, use) => {
    const dashboardPage = new DashboardPage(page);
    await use(dashboardPage);
  },

  // Pre-authenticated page fixture
  authenticatedPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login(
      process.env.TEST_USER_EMAIL || 'test@example.com',
      process.env.TEST_USER_PASSWORD || 'password123'
    );
    
    // Page is now authenticated
    await use(page);
    
    // Cleanup: Logout
    const dashboardPage = new DashboardPage(page);
    await dashboardPage.logout();
  }
});

export { expect } from '@playwright/test';
TypeScript - Advanced Fixtures
// fixtures/pages.fixture.ts
import { test as base, Page } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
import { CheckoutPage } from '../pages/CheckoutPage';

type PageFixtures = {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
  checkoutPage: CheckoutPage;
};

export const test = base.extend<PageFixtures>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  
  dashboardPage: async ({ page }, use) => {
    await use(new DashboardPage(page));
  },
  
  checkoutPage: async ({ page }, use) => {
    await use(new CheckoutPage(page));
  }
});

// fixtures/api.fixture.ts
import { test as base, APIRequestContext } from '@playwright/test';

type APIFixtures = {
  apiContext: APIRequestContext;
};

export const test = base.extend<APIFixtures>({
  apiContext: async ({ playwright }, use) => {
    const context = await playwright.request.newContext({
      baseURL: process.env.API_URL || 'https://api.example.com',
      extraHTTPHeaders: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      }
    });
    
    await use(context);
    await context.dispose();
  }
});
TypeScript - Using Custom Fixtures
import { test, expect } from '../fixtures/test';

test.describe('Login Tests', () => {
  test('successful login with fixtures', async ({ loginPage, dashboardPage }) => {
    await loginPage.goto();
    await loginPage.login('user@example.com', 'password123');
    
    await expect(dashboardPage.isLoaded()).toBeTruthy();
  });

  test('test with authenticated page', async ({ authenticatedPage }) => {
    // Already logged in!
    await authenticatedPage.goto('/settings');
    
    await expect(authenticatedPage.getByRole('heading'))
      .toHaveText('Settings');
  });
});

4. Configuration Management

Playwright Config

Configuration
TypeScript - playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';

// Load environment variables
dotenv.config();

export default defineConfig({
  // Test directory
  testDir: './tests',
  
  // Test match pattern
  testMatch: '**/*.spec.ts',
  
  // Timeout settings
  timeout: 60000,
  expect: {
    timeout: 10000
  },
  
  // Global setup/teardown
  globalSetup: require.resolve('./setup/global-setup'),
  globalTeardown: require.resolve('./setup/global-teardown'),
  
  // Run tests in parallel
  fullyParallel: true,
  
  // Retry on failure
  retries: process.env.CI ? 2 : 0,
  
  // Number of workers
  workers: process.env.CI ? 2 : undefined,
  
  // Fail fast
  maxFailures: process.env.CI ? 10 : undefined,
  
  // Reporter configuration
  reporter: [
    ['html', { outputFolder: 'playwright-report', open: 'never' }],
    ['json', { outputFile: 'test-results/results.json' }],
    ['junit', { outputFile: 'test-results/junit.xml' }],
    ['list']
  ],
  
  // Shared settings for all projects
  use: {
    // Base URL
    baseURL: process.env.BASE_URL || 'https://example.com',
    
    // Browser settings
    headless: process.env.HEADED !== 'true',
    viewport: { width: 1280, height: 720 },
    
    // Screenshots and videos
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'retain-on-failure',
    
    // Timeouts
    navigationTimeout: 30000,
    actionTimeout: 10000,
    
    // Locale and timezone
    locale: 'en-US',
    timezoneId: 'America/New_York',
    
    // Ignore HTTPS errors
    ignoreHTTPSErrors: true,
  },
  
  // Projects for different browsers
  projects: [
    // Setup project
    {
      name: 'setup',
      testMatch: /.*.setup.ts/
    },
    
    // Chromium
    {
      name: 'chromium',
      use: { 
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json'
      },
      dependencies: ['setup']
    },
    
    // Firefox
    {
      name: 'firefox',
      use: { 
        ...devices['Desktop Firefox'],
        storageState: 'playwright/.auth/user.json'
      },
      dependencies: ['setup']
    },
    
    // WebKit
    {
      name: 'webkit',
      use: { 
        ...devices['Desktop Safari'],
        storageState: 'playwright/.auth/user.json'
      },
      dependencies: ['setup']
    },
    
    // Mobile Chrome
    {
      name: 'mobile',
      use: { 
        ...devices['Pixel 5'],
        storageState: 'playwright/.auth/user.json'
      },
      dependencies: ['setup']
    }
  ],
  
  // Web server (optional - start dev server)
  // webServer: {
  //   command: 'npm run start',
  //   port: 3000,
  //   reuseExistingServer: !process.env.CI,
  //   timeout: 120000
  // }
});

Environment Configuration

Configuration
TypeScript - config/env.config.ts
export class EnvConfig {
  static getBaseURL(): string {
    const env = process.env.ENV || 'qa';
    const urls = {
      dev: 'https://dev.example.com',
      qa: 'https://qa.example.com',
      staging: 'https://staging.example.com',
      prod: 'https://www.example.com'
    };
    return urls[env as keyof typeof urls] || urls.qa;
  }

  static getAPIURL(): string {
    const env = process.env.ENV || 'qa';
    const urls = {
      dev: 'https://api-dev.example.com',
      qa: 'https://api-qa.example.com',
      staging: 'https://api-staging.example.com',
      prod: 'https://api.example.com'
    };
    return urls[env as keyof typeof urls] || urls.qa;
  }

  static getTestUserEmail(): string {
    return process.env.TEST_USER_EMAIL || 'test@example.com';
  }

  static getTestUserPassword(): string {
    return process.env.TEST_USER_PASSWORD || 'password123';
  }

  static isHeadless(): boolean {
    return process.env.HEADED !== 'true';
  }

  static getTimeout(): number {
    return parseInt(process.env.TIMEOUT || '30000');
  }
}
.env.example
# Environment
ENV=qa

# URLs
BASE_URL=https://qa.example.com
API_URL=https://api-qa.example.com

# Test Credentials
TEST_USER_EMAIL=test@example.com
TEST_USER_PASSWORD=password123

# Browser Settings
HEADED=false
TIMEOUT=30000

# CI/CD
CI=false

5. Authentication State Management

Global Authentication Setup

Setup
TypeScript - setup/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import path from 'path';

const authFile = path.join(__dirname, '../playwright/.auth/user.json');

setup('authenticate', async ({ page }) => {
  const loginPage = new LoginPage(page);
  
  // Navigate to login page
  await loginPage.goto();
  
  // Perform login
  await loginPage.login(
    process.env.TEST_USER_EMAIL || 'test@example.com',
    process.env.TEST_USER_PASSWORD || 'password123'
  );
  
  // Wait for successful login
  await page.waitForURL('**/dashboard');
  
  // Verify login success
  await expect(page.getByRole('heading')).toContainText('Dashboard');
  
  // Save authentication state
  await page.context().storageState({ path: authFile });
  
  console.log('āœ… Authentication state saved');
});
TypeScript - Multiple Users
// setup/auth.setup.ts - Extended
const adminAuthFile = path.join(__dirname, '../playwright/.auth/admin.json');
const userAuthFile = path.join(__dirname, '../playwright/.auth/user.json');

setup('authenticate as admin', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('admin@example.com', 'admin123');
  await page.waitForURL('**/admin');
  await page.context().storageState({ path: adminAuthFile });
});

setup('authenticate as user', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@example.com', 'user123');
  await page.waitForURL('**/dashboard');
  await page.context().storageState({ path: userAuthFile });
});

6. Test Organization & Data-Driven Testing

Test Structure & Organization

Test Organization
TypeScript - tests/auth/login.spec.ts
import { test, expect } from '../../fixtures/test';

test.describe('Login Functionality', () => {
  // Run before each test in this describe block
  test.beforeEach(async ({ loginPage }) => {
    await loginPage.goto();
  });

  test('successful login with valid credentials @smoke', async ({ loginPage, dashboardPage }) => {
    await loginPage.login('user@example.com', 'password123');
    
    await expect(dashboardPage.isLoaded()).toBeTruthy();
    const welcomeMsg = await dashboardPage.getWelcomeMessage();
    expect(welcomeMsg).toContain('Welcome');
  });

  test('failed login with invalid credentials @regression', async ({ loginPage }) => {
    await loginPage.login('invalid@example.com', 'wrongpassword');
    
    expect(await loginPage.isErrorDisplayed()).toBeTruthy();
    const errorMsg = await loginPage.getErrorMessage();
    expect(errorMsg).toContain('Invalid credentials');
  });

  test('login with empty email @regression', async ({ loginPage }) => {
    await loginPage.enterPassword('password123');
    await loginPage.clickLogin();
    
    expect(await loginPage.isErrorDisplayed()).toBeTruthy();
  });

  test('login with empty password @regression', async ({ loginPage }) => {
    await loginPage.enterEmail('user@example.com');
    await loginPage.clickLogin();
    
    expect(await loginPage.isErrorDisplayed()).toBeTruthy();
  });
});

// Skip tests conditionally
test.describe('Browser-specific tests', () => {
  test('webkit only feature', async ({ page, browserName }) => {
    test.skip(browserName !== 'webkit', 'Safari-only feature');
    
    // Test Safari-specific functionality
  });
});

// Serial execution (tests run in order)
test.describe.serial('Dependent tests', () => {
  let userId: string;

  test('create user', async ({ page }) => {
    // Create user and get ID
    userId = '123';
  });

  test('update user', async ({ page }) => {
    // Use userId from previous test
    console.log(userId);
  });
});

// Parallel execution (default)
test.describe.parallel('Independent tests', () => {
  test('test 1', async ({ page }) => {
    // Runs in parallel
  });

  test('test 2', async ({ page }) => {
    // Runs in parallel
  });
});

Data-Driven Testing

Test Data
JSON - test-data/users.json
{
  "validUsers": [
    {
      "email": "user1@example.com",
      "password": "password123",
      "expectedName": "John Doe"
    },
    {
      "email": "user2@example.com",
      "password": "password456",
      "expectedName": "Jane Smith"
    }
  ],
  "invalidUsers": [
    {
      "email": "invalid@example.com",
      "password": "wrongpass",
      "expectedError": "Invalid credentials"
    },
    {
      "email": "notfound@example.com",
      "password": "password",
      "expectedError": "User not found"
    }
  ]
}
TypeScript - Data-Driven Test
import { test, expect } from '../../fixtures/test';
import users from '../../test-data/users.json';

test.describe('Login with multiple users', () => {
  // Loop through test data
  for (const user of users.validUsers) {
    test(`login as ${user.email}`, async ({ loginPage, dashboardPage }) => {
      await loginPage.goto();
      await loginPage.login(user.email, user.password);
      
      const welcomeMsg = await dashboardPage.getWelcomeMessage();
      expect(welcomeMsg).toContain(user.expectedName);
    });
  }

  for (const user of users.invalidUsers) {
    test(`failed login: ${user.email}`, async ({ loginPage }) => {
      await loginPage.goto();
      await loginPage.login(user.email, user.password);
      
      const errorMsg = await loginPage.getErrorMessage();
      expect(errorMsg).toContain(user.expectedError);
    });
  }
});

// Using test.describe.configure for each
test.describe('Parameterized tests', () => {
  const testCases = [
    { input: 'test@example.com', expected: true },
    { input: 'invalid-email', expected: false },
    { input: '', expected: false }
  ];

  for (const testCase of testCases) {
    test(`validate email: ${testCase.input}`, async ({ page }) => {
      // Test logic here
    });
  }
});
TypeScript - Test Data Helper
// utils/test-data.ts
import { faker } from '@faker-js/faker';

export class TestDataGenerator {
  static generateUser() {
    return {
      firstName: faker.person.firstName(),
      lastName: faker.person.lastName(),
      email: faker.internet.email(),
      password: faker.internet.password({ length: 12 }),
      phone: faker.phone.number()
    };
  }

  static generateAddress() {
    return {
      street: faker.location.streetAddress(),
      city: faker.location.city(),
      state: faker.location.state(),
      zipCode: faker.location.zipCode(),
      country: faker.location.country()
    };
  }

  static generateCreditCard() {
    return {
      number: '4111111111111111', // Test card
      cvv: faker.finance.creditCardCVV(),
      expiryMonth: '12',
      expiryYear: '2025'
    };
  }
}

// Usage
const user = TestDataGenerator.generateUser();

7. API Testing & Hybrid Tests

API Helper Class

API Testing
TypeScript - utils/api-helpers.ts
import { APIRequestContext } from '@playwright/test';

export class APIHelper {
  private request: APIRequestContext;
  private baseURL: string;

  constructor(request: APIRequestContext) {
    this.request = request;
    this.baseURL = process.env.API_URL || 'https://api.example.com';
  }

  /**
   * Create user via API
   */
  async createUser(userData: any) {
    const response = await this.request.post(`${this.baseURL}/users`, {
      data: userData
    });
    
    if (!response.ok()) {
      throw new Error(`Failed to create user: ${response.status()}`);
    }
    
    return await response.json();
  }

  /**
   * Get user by ID
   */
  async getUser(userId: string) {
    const response = await this.request.get(`${this.baseURL}/users/${userId}`);
    
    if (!response.ok()) {
      throw new Error(`Failed to get user: ${response.status()}`);
    }
    
    return await response.json();
  }

  /**
   * Update user
   */
  async updateUser(userId: string, updates: any) {
    const response = await this.request.put(`${this.baseURL}/users/${userId}`, {
      data: updates
    });
    
    if (!response.ok()) {
      throw new Error(`Failed to update user: ${response.status()}`);
    }
    
    return await response.json();
  }

  /**
   * Delete user
   */
  async deleteUser(userId: string) {
    const response = await this.request.delete(`${this.baseURL}/users/${userId}`);
    
    if (!response.ok() && response.status() !== 204) {
      throw new Error(`Failed to delete user: ${response.status()}`);
    }
  }

  /**
   * Create product (example)
   */
  async createProduct(productData: any) {
    const response = await this.request.post(`${this.baseURL}/products`, {
      data: productData
    });
    
    return await response.json();
  }

  /**
   * Set authentication token
   */
  async setAuthToken(token: string) {
    await this.request.dispose();
    // Create new context with token
  }
}
TypeScript - Hybrid Test Example
import { test, expect } from '../fixtures/test';
import { APIHelper } from '../utils/api-helpers';
import { TestDataGenerator } from '../utils/test-data';

test.describe('User Management - Hybrid Test', () => {
  let apiHelper: APIHelper;
  let createdUserId: string;

  test.beforeEach(async ({ request }) => {
    apiHelper = new APIHelper(request);
  });

  test('create user via API, verify in UI', async ({ page, request }) => {
    // SETUP: Create user via API (fast)
    const userData = TestDataGenerator.generateUser();
    const response = await apiHelper.createUser(userData);
    createdUserId = response.id;

    // TEST: Verify in UI
    await page.goto(`/users/${createdUserId}`);
    
    await expect(page.getByRole('heading')).toHaveText(
      `${userData.firstName} ${userData.lastName}`
    );
    await expect(page.locator('[data-testid="email"]')).toHaveText(userData.email);
  });

  test('update user in UI, verify via API', async ({ page, request }) => {
    // SETUP: Create user via API
    const userData = TestDataGenerator.generateUser();
    const user = await apiHelper.createUser(userData);
    createdUserId = user.id;

    // TEST: Update in UI
    await page.goto(`/users/${user.id}/edit`);
    await page.fill('input[name="firstName"]', 'Updated Name');
    await page.click('button[type="submit"]');

    // VERIFY: Check via API
    const updatedUser = await apiHelper.getUser(user.id);
    expect(updatedUser.firstName).toBe('Updated Name');
  });

  test.afterEach(async () => {
    // CLEANUP: Delete user via API
    if (createdUserId) {
      await apiHelper.deleteUser(createdUserId);
    }
  });
});

// Pure API test
test.describe('API Tests', () => {
  test('CRUD operations via API', async ({ request }) => {
    const apiHelper = new APIHelper(request);

    // CREATE
    const userData = TestDataGenerator.generateUser();
    const created = await apiHelper.createUser(userData);
    expect(created.id).toBeDefined();

    // READ
    const retrieved = await apiHelper.getUser(created.id);
    expect(retrieved.email).toBe(userData.email);

    // UPDATE
    const updated = await apiHelper.updateUser(created.id, {
      firstName: 'Updated'
    });
    expect(updated.firstName).toBe('Updated');

    // DELETE
    await apiHelper.deleteUser(created.id);
  });
});

8. Reporting & Logging

Custom Reporter

Reporting
TypeScript - reporters/custom-reporter.ts
import { Reporter, TestCase, TestResult } from '@playwright/test/reporter';

class CustomReporter implements Reporter {
  private passedTests = 0;
  private failedTests = 0;
  private skippedTests = 0;
  private startTime: number = 0;

  onBegin() {
    this.startTime = Date.now();
    console.log('\nšŸš€ Test execution started\n');
  }

  onTestEnd(test: TestCase, result: TestResult) {
    const duration = (result.duration / 1000).toFixed(2);
    
    if (result.status === 'passed') {
      this.passedTests++;
      console.log(`āœ… PASSED: ${test.title} (${duration}s)`);
    } else if (result.status === 'failed') {
      this.failedTests++;
      console.log(`āŒ FAILED: ${test.title} (${duration}s)`);
      console.log(`   Error: ${result.error?.message}`);
    } else if (result.status === 'skipped') {
      this.skippedTests++;
      console.log(`ā­ļø  SKIPPED: ${test.title}`);
    }
  }

  onEnd() {
    const totalDuration = ((Date.now() - this.startTime) / 1000).toFixed(2);
    const total = this.passedTests + this.failedTests + this.skippedTests;
    
    console.log('\n' + '='.repeat(60));
    console.log('šŸ“Š Test Execution Summary');
    console.log('='.repeat(60));
    console.log(`Total Tests:    ${total}`);
    console.log(`āœ… Passed:      ${this.passedTests}`);
    console.log(`āŒ Failed:      ${this.failedTests}`);
    console.log(`ā­ļø  Skipped:     ${this.skippedTests}`);
    console.log(`ā±ļø  Duration:    ${totalDuration}s`);
    console.log('='.repeat(60) + '\n');
  }
}

export default CustomReporter;
TypeScript - Add to Config
// playwright.config.ts
reporter: [
  ['html', { outputFolder: 'playwright-report', open: 'never' }],
  ['json', { outputFile: 'test-results/results.json' }],
  ['junit', { outputFile: 'test-results/junit.xml' }],
  ['./reporters/custom-reporter.ts'],
  ['list']
]
TypeScript - Logger Utility
// utils/logger.ts
import fs from 'fs';
import path from 'path';

export class Logger {
  private logFile: string;

  constructor() {
    const timestamp = new Date().toISOString().replace(/:/g, '-');
    this.logFile = path.join('logs', `test-${timestamp}.log`);
    
    // Ensure logs directory exists
    if (!fs.existsSync('logs')) {
      fs.mkdirSync('logs');
    }
  }

  info(message: string) {
    this.log('INFO', message);
  }

  error(message: string) {
    this.log('ERROR', message);
  }

  warn(message: string) {
    this.log('WARN', message);
  }

  debug(message: string) {
    this.log('DEBUG', message);
  }

  private log(level: string, message: string) {
    const timestamp = new Date().toISOString();
    const logMessage = `[${timestamp}] [${level}] ${message}\n`;
    
    // Console output
    console.log(logMessage.trim());
    
    // File output
    fs.appendFileSync(this.logFile, logMessage);
  }
}

// Singleton instance
export const logger = new Logger();

9. CI/CD Integration

GitHub Actions

CI/CD
.github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]
  schedule:
    - cron: '0 0 * * *'  # Daily at midnight
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment to test'
        required: true
        default: 'qa'
        type: choice
        options:
          - dev
          - qa
          - staging

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        browser: [chromium, firefox, webkit]
      fail-fast: false
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Install Playwright browsers
        run: npx playwright install --with-deps ${{ matrix.browser }}
      
      - name: Create .env file
        run: |
          echo "ENV=${{ github.event.inputs.environment || 'qa' }}" >> .env
          echo "BASE_URL=${{ secrets.BASE_URL }}" >> .env
          echo "API_URL=${{ secrets.API_URL }}" >> .env
          echo "TEST_USER_EMAIL=${{ secrets.TEST_USER_EMAIL }}" >> .env
          echo "TEST_USER_PASSWORD=${{ secrets.TEST_USER_PASSWORD }}" >> .env
      
      - name: Run Playwright tests
        run: npx playwright test --project=${{ matrix.browser }}
        env:
          CI: true
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report-${{ matrix.browser }}
          path: playwright-report/
          retention-days: 30
      
      - name: Upload test traces
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-traces-${{ matrix.browser }}
          path: test-results/
          retention-days: 7
      
      - name: Publish test results
        if: always()
        uses: EnricoMi/publish-unit-test-result-action@v2
        with:
          files: test-results/junit.xml
          check_name: Test Results (${{ matrix.browser }})
      
      - name: Comment PR with results
        if: github.event_name == 'pull_request' && always()
        uses: daun/playwright-report-comment@v3
        with:
          report-url: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/${{ github.run_id }}

  deploy-report:
    if: always()
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: all-reports
      
      - name: Merge reports
        run: |
          mkdir -p merged-report
          cp -r all-reports/**/. merged-report/
      
      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./merged-report
          destination_dir: ${{ github.run_id }}

GitLab CI

CI/CD
.gitlab-ci.yml
image: mcr.microsoft.com/playwright:v1.40.0-jammy

stages:
  - test
  - report

variables:
  ENV: "qa"
  npm_config_cache: "$CI_PROJECT_DIR/.npm"

cache:
  paths:
    - .npm
    - node_modules

before_script:
  - npm ci
  - npx playwright install

test:chromium:
  stage: test
  script:
    - npx playwright test --project=chromium
  artifacts:
    when: always
    paths:
      - playwright-report/
      - test-results/
    expire_in: 1 week
  retry:
    max: 2
    when:
      - runner_system_failure
      - stuck_or_timeout_failure

test:firefox:
  stage: test
  script:
    - npx playwright test --project=firefox
  artifacts:
    when: always
    paths:
      - playwright-report/
      - test-results/
    expire_in: 1 week

test:webkit:
  stage: test
  script:
    - npx playwright test --project=webkit
  artifacts:
    when: always
    paths:
      - playwright-report/
      - test-results/
    expire_in: 1 week

pages:
  stage: report
  dependencies:
    - test:chromium
    - test:firefox
    - test:webkit
  script:
    - mkdir -p public
    - cp -r playwright-report/* public/
  artifacts:
    paths:
      - public
  only:
    - main

Docker Support

Containerization
Dockerfile
FROM mcr.microsoft.com/playwright:v1.40.0-jammy

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci

# Copy project files
COPY . .

# Run tests
CMD ["npx", "playwright", "test"]
docker-compose.yml
version: '3.8'

services:
  playwright-tests:
    build: .
    environment:
      - ENV=qa
      - CI=true
    volumes:
      - ./playwright-report:/app/playwright-report
      - ./test-results:/app/test-results
    command: npx playwright test

  playwright-ui:
    build: .
    ports:
      - "9323:9323"
    environment:
      - ENV=qa
    volumes:
      - .:/app
    command: npx playwright test --ui-host=0.0.0.0

10. Best Practices & Anti-patterns

Framework Best Practices

Best Practices
Categoryāœ… Best PracticeāŒ Anti-pattern
LocatorsUse getByRole(), getByTestId()Use CSS/XPath based on structure
WaitsUse auto-waiting locatorsUse page.waitForTimeout()
AssertionsUse web-first assertionsGet value then assert (no retry)
Page ObjectsUse fixtures for dependency injectionManually instantiate in tests
Test DataUse API for setup/teardownUse UI for test data creation
AuthenticationSave and reuse auth stateLogin before every test
TypeScriptUse strict typing, interfacesUse 'any' type everywhere
OrganizationIndependent, isolated testsTests depend on each other

Common Anti-patterns to Avoid

Anti-patterns
āŒ Anti-pattern #1: Not Using Auto-Waiting
// āŒ BAD: Manual waits
await page.waitForSelector('.content');
await page.click('.content');

// āœ… GOOD: Auto-waiting
await page.locator('.content').click();
āŒ Anti-pattern #2: Not Using Fixtures
// āŒ BAD: Manual instantiation
test('bad example', async ({ page }) => {
  const loginPage = new LoginPage(page);
  // ...
});

// āœ… GOOD: Use fixtures
test('good example', async ({ loginPage }) => {
  // Already instantiated and ready
});
āŒ Anti-pattern #3: Using UI for Test Data Setup
// āŒ BAD: Create data via UI (slow)
test('bad setup', async ({ page }) => {
  await page.goto('/admin/users');
  await page.click('button.add-user');
  await page.fill('input[name="email"]', 'test@example.com');
  await page.click('button.save');
  // Now test actual functionality
});

// āœ… GOOD: Create data via API (fast)
test('good setup', async ({ page, request }) => {
  const user = await request.post('/api/users', {
    data: { email: 'test@example.com' }
  });
  
  // Test with existing user
  await page.goto(`/users/${user.id}`);
});
āŒ Anti-pattern #4: Test Interdependence
// āŒ BAD: Dependent tests
test.describe.serial('bad tests', () => {
  test('create user', async () => {
    // Creates user, leaves browser in specific state
  });

  test('edit user', async () => {
    // Assumes user exists from previous test
  });
});

// āœ… GOOD: Independent tests
test('create user', async ({ page, request }) => {
  // Setup, execute, verify, cleanup all in one test
});

test('edit user', async ({ page, request }) => {
  // Create user via API, test edit, cleanup
});

Code Quality Checklist

Quality
āœ… Framework Quality Checklist:
  • āœ… All code uses TypeScript with strict mode
  • āœ… Page Objects use dependency injection via fixtures
  • āœ… Tests are independent and can run in any order
  • āœ… Authentication state is saved and reused
  • āœ… Test data setup uses API (not UI)
  • āœ… Locators use getByRole() or data-testid
  • āœ… Web-first assertions (auto-retry) are used
  • āœ… No page.waitForTimeout() in tests
  • āœ… Parallel execution enabled
  • āœ… CI/CD pipeline configured
  • āœ… Reports generated automatically
  • āœ… Environment-specific configs
šŸ’” Framework Maintenance:
  • Version Control: Use Git with meaningful commits
  • Code Reviews: All changes peer-reviewed
  • Documentation: Maintain README with setup instructions
  • Dependencies: Keep Playwright updated
  • Monitoring: Track flaky tests and execution trends
  • Refactoring: Regular code cleanup

Framework Implementation Checklist

āœ… Complete Framework Components:

Project Setup

  • āœ… TypeScript configuration with strict mode
  • āœ… Playwright config with multiple projects
  • āœ… Environment-specific configurations
  • āœ… Package.json with useful scripts

Page Objects

  • āœ… BasePage with common methods
  • āœ… Page-specific classes extending BasePage
  • āœ… Type-safe locators and methods
  • āœ… No assertions in page objects

Fixtures & Tests

  • āœ… Custom fixtures for page objects
  • āœ… Authentication state management
  • āœ… Tests organized by feature
  • āœ… Data-driven testing setup

API & Utilities

  • āœ… API helper for hybrid testing
  • āœ… Test data generators
  • āœ… Logger utility
  • āœ… Custom reporters

CI/CD

  • āœ… GitHub Actions / GitLab CI pipeline
  • āœ… Docker support
  • āœ… Automated reporting
  • āœ… Parallel execution

šŸŽ‰ You're Ready to Build Production Frameworks!

This comprehensive guide covered everything from project setup to CI/CD integration. You now have the knowledge to build enterprise-grade Playwright test frameworks with TypeScript.

šŸš€ Next Steps:
  • Implement the framework structure in your project
  • Convert existing tests to use Page Objects and fixtures
  • Set up CI/CD pipeline for automated execution
  • Add API testing for faster test data setup
  • Monitor and optimize test execution time

Continue Learning