Skip to content

Latest commit

 

History

History
714 lines (602 loc) · 20.1 KB

File metadata and controls

714 lines (602 loc) · 20.1 KB
name Playwright End-to-End Testing Automation Guide
description A comprehensive guide for QA automation engineers using Playwright with TypeScript and JavaScript for reliable end-to-end testing across web applications
category Testing Framework
author Agents.md Collection
authorUrl https://github.com/gakeez/agents_md_collection
tags
playwright
e2e-testing
typescript
javascript
qa-automation
testing
web-testing
cross-browser
lastUpdated 2025-06-16

Playwright End-to-End Testing Automation Guide

Project Overview

This comprehensive guide outlines best practices for QA automation engineers using Playwright with TypeScript and JavaScript for end-to-end testing. The guide emphasizes writing reliable, maintainable tests that reflect real user behavior, utilizing Playwright's modern testing capabilities including fixtures, web-first assertions, and cross-browser compatibility.

Tech Stack

  • Testing Framework: Playwright 1.40+
  • Languages: TypeScript 5.0+, JavaScript ES2022+
  • Test Runner: Playwright Test Runner
  • Browsers: Chromium, Firefox, WebKit (Safari)
  • Devices: Desktop, Mobile, Tablet configurations
  • CI/CD: GitHub Actions, Jenkins, Azure DevOps
  • Reporting: HTML Reporter, Allure, JUnit XML
  • Visual Testing: Playwright Screenshots, Percy integration

Development Environment Setup

Installation Requirements

  • Node.js 18+
  • npm/yarn/pnpm
  • Playwright browsers
  • VS Code with Playwright extension (recommended)

Installation Steps

# Initialize new project
npm init playwright@latest

# Or add to existing project
npm install -D @playwright/test
npx playwright install

# Install specific browsers
npx playwright install chromium firefox webkit

# Install system dependencies (Linux)
npx playwright install-deps

Project Structure

e2e-tests/
├── tests/                          # Test files
│   ├── auth/
│   │   ├── login.spec.ts
│   │   └── registration.spec.ts
│   ├── user-management/
│   │   ├── profile.spec.ts
│   │   └── settings.spec.ts
│   └── api/
│       └── user-api.spec.ts
├── fixtures/                       # Custom fixtures
│   ├── auth-fixture.ts
│   └── database-fixture.ts
├── page-objects/                   # Page Object Models
│   ├── login-page.ts
│   ├── dashboard-page.ts
│   └── base-page.ts
├── utils/                          # Helper utilities
│   ├── test-data.ts
│   ├── api-helpers.ts
│   └── database-helpers.ts
├── config/                         # Configuration files
│   ├── environments.ts
│   └── test-data.json
├── reports/                        # Test reports
├── screenshots/                    # Visual comparisons
├── playwright.config.ts           # Main configuration
├── global-setup.ts               # Global setup
├── global-teardown.ts            # Global teardown
└── package.json

Core Testing Principles

Test Structure and Naming

// tests/user-management/profile.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../page-objects/login-page";
import { ProfilePage } from "../page-objects/profile-page";

test.describe("User Profile Management", () => {
  test.beforeEach(async ({ page }) => {
    // Setup: Navigate to application and login
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.loginWithValidCredentials(
      "user@example.com",
      "password123"
    );
  });

  test("should display user profile information correctly", async ({
    page,
  }) => {
    const profilePage = new ProfilePage(page);
    await profilePage.goto();

    // Verify profile information is displayed
    await expect(profilePage.userNameField).toBeVisible();
    await expect(profilePage.emailField).toHaveValue("user@example.com");
    await expect(profilePage.profileImage).toBeVisible();
  });

  test("should update user profile successfully", async ({ page }) => {
    const profilePage = new ProfilePage(page);
    await profilePage.goto();

    const newName = "Updated User Name";
    await profilePage.updateUserName(newName);
    await profilePage.saveChanges();

    // Verify success message and updated information
    await expect(profilePage.successMessage).toBeVisible();
    await expect(profilePage.successMessage).toHaveText(
      "Profile updated successfully"
    );
    await expect(profilePage.userNameField).toHaveValue(newName);
  });

  test("should validate required fields when updating profile", async ({
    page,
  }) => {
    const profilePage = new ProfilePage(page);
    await profilePage.goto();

    // Clear required field and attempt to save
    await profilePage.clearUserName();
    await profilePage.saveChanges();

    // Verify validation error
    await expect(profilePage.nameValidationError).toBeVisible();
    await expect(profilePage.nameValidationError).toHaveText(
      "Name is required"
    );
  });
});

Page Object Model Implementation

// page-objects/base-page.ts
import { Page, Locator } from "@playwright/test";

export abstract class BasePage {
  protected page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  /**
   * Wait for the page to be fully loaded
   */
  async waitForPageLoad(): Promise<void> {
    await this.page.waitForLoadState("networkidle");
  }

  /**
   * Take a screenshot of the current page
   */
  async takeScreenshot(name: string): Promise<void> {
    await this.page.screenshot({
      path: `screenshots/${name}.png`,
      fullPage: true,
    });
  }
}

// page-objects/login-page.ts
import { Page, Locator, expect } from "@playwright/test";
import { BasePage } from "./base-page";

export class LoginPage extends BasePage {
  // Use role-based and semantic locators
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;
  readonly forgotPasswordLink: Locator;

  constructor(page: Page) {
    super(page);
    this.emailInput = page.getByLabel("Email");
    this.passwordInput = page.getByLabel("Password");
    this.loginButton = page.getByRole("button", { name: "Sign In" });
    this.errorMessage = page.getByTestId("login-error");
    this.forgotPasswordLink = page.getByRole("link", {
      name: "Forgot Password?",
    });
  }

  async goto(): Promise<void> {
    await this.page.goto("/login");
    await this.waitForPageLoad();
  }

  /**
   * Login with provided credentials
   */
  async loginWithCredentials(email: string, password: string): Promise<void> {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  /**
   * Login with valid test credentials
   */
  async loginWithValidCredentials(
    email?: string,
    password?: string
  ): Promise<void> {
    const testEmail =
      email || process.env.TEST_USER_EMAIL || "test@example.com";
    const testPassword =
      password || process.env.TEST_USER_PASSWORD || "password123";

    await this.loginWithCredentials(testEmail, testPassword);
    await this.page.waitForURL("/dashboard");
  }

  /**
   * Verify login error is displayed
   */
  async verifyLoginError(expectedMessage: string): Promise<void> {
    await expect(this.errorMessage).toBeVisible();
    await expect(this.errorMessage).toHaveText(expectedMessage);
  }
}

// page-objects/profile-page.ts
import { Page, Locator } from "@playwright/test";
import { BasePage } from "./base-page";

export class ProfilePage extends BasePage {
  readonly userNameField: Locator;
  readonly emailField: Locator;
  readonly profileImage: Locator;
  readonly saveButton: Locator;
  readonly successMessage: Locator;
  readonly nameValidationError: Locator;

  constructor(page: Page) {
    super(page);
    this.userNameField = page.getByLabel("Full Name");
    this.emailField = page.getByLabel("Email Address");
    this.profileImage = page.getByRole("img", { name: "Profile Picture" });
    this.saveButton = page.getByRole("button", { name: "Save Changes" });
    this.successMessage = page.getByTestId("success-message");
    this.nameValidationError = page.getByTestId("name-validation-error");
  }

  async goto(): Promise<void> {
    await this.page.goto("/profile");
    await this.waitForPageLoad();
  }

  /**
   * Update user name field
   */
  async updateUserName(name: string): Promise<void> {
    await this.userNameField.clear();
    await this.userNameField.fill(name);
  }

  /**
   * Clear user name field
   */
  async clearUserName(): Promise<void> {
    await this.userNameField.clear();
  }

  /**
   * Save profile changes
   */
  async saveChanges(): Promise<void> {
    await this.saveButton.click();
  }
}

Configuration and Setup

Playwright Configuration

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ["html"],
    ["junit", { outputFile: "reports/junit-results.xml" }],
    ["json", { outputFile: "reports/test-results.json" }],
  ],
  use: {
    baseURL: process.env.BASE_URL || "http://localhost:3000",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
    actionTimeout: 10000,
    navigationTimeout: 30000,
  },
  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
    {
      name: "firefox",
      use: { ...devices["Desktop Firefox"] },
    },
    {
      name: "webkit",
      use: { ...devices["Desktop Safari"] },
    },
    {
      name: "Mobile Chrome",
      use: { ...devices["Pixel 5"] },
    },
    {
      name: "Mobile Safari",
      use: { ...devices["iPhone 12"] },
    },
    {
      name: "Microsoft Edge",
      use: { ...devices["Desktop Edge"], channel: "msedge" },
    },
  ],
  webServer: {
    command: "npm run start",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
  },
});

Custom Fixtures

// fixtures/auth-fixture.ts
import { test as base, expect } from "@playwright/test";
import { LoginPage } from "../page-objects/login-page";

type AuthFixtures = {
  authenticatedPage: any;
  loginPage: LoginPage;
};

export const test = base.extend<AuthFixtures>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
  },

  authenticatedPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.loginWithValidCredentials();
    await use(page);
  },
});

export { expect };

Environment Configuration

// config/environments.ts
export interface Environment {
  baseUrl: string;
  apiUrl: string;
  testUser: {
    email: string;
    password: string;
  };
  adminUser: {
    email: string;
    password: string;
  };
}

export const environments: Record<string, Environment> = {
  development: {
    baseUrl: "http://localhost:3000",
    apiUrl: "http://localhost:3001/api",
    testUser: {
      email: "test@example.com",
      password: "password123",
    },
    adminUser: {
      email: "admin@example.com",
      password: "admin123",
    },
  },
  staging: {
    baseUrl: "https://staging.example.com",
    apiUrl: "https://api-staging.example.com",
    testUser: {
      email: process.env.STAGING_TEST_EMAIL!,
      password: process.env.STAGING_TEST_PASSWORD!,
    },
    adminUser: {
      email: process.env.STAGING_ADMIN_EMAIL!,
      password: process.env.STAGING_ADMIN_PASSWORD!,
    },
  },
  production: {
    baseUrl: "https://example.com",
    apiUrl: "https://api.example.com",
    testUser: {
      email: process.env.PROD_TEST_EMAIL!,
      password: process.env.PROD_TEST_PASSWORD!,
    },
    adminUser: {
      email: process.env.PROD_ADMIN_EMAIL!,
      password: process.env.PROD_ADMIN_PASSWORD!,
    },
  },
};

export function getEnvironment(): Environment {
  const env = process.env.NODE_ENV || "development";
  return environments[env];
}

Advanced Testing Patterns

API Testing Integration

// tests/api/user-api.spec.ts
import { test, expect } from "@playwright/test";

test.describe("User API Tests", () => {
  let apiContext: any;

  test.beforeAll(async ({ playwright }) => {
    apiContext = await playwright.request.newContext({
      baseURL: "https://api.example.com",
      extraHTTPHeaders: {
        Authorization: `Bearer ${process.env.API_TOKEN}`,
        "Content-Type": "application/json",
      },
    });
  });

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

  test("should create user via API", async () => {
    const userData = {
      name: "Test User",
      email: "testuser@example.com",
      password: "securePassword123",
    };

    const response = await apiContext.post("/users", {
      data: userData,
    });

    expect(response.status()).toBe(201);

    const responseBody = await response.json();
    expect(responseBody).toHaveProperty("id");
    expect(responseBody.email).toBe(userData.email);
    expect(responseBody.name).toBe(userData.name);
  });

  test("should retrieve user by ID", async () => {
    // First create a user
    const createResponse = await apiContext.post("/users", {
      data: {
        name: "Retrieve Test User",
        email: "retrieve@example.com",
        password: "password123",
      },
    });

    const createdUser = await createResponse.json();

    // Then retrieve the user
    const getResponse = await apiContext.get(`/users/${createdUser.id}`);
    expect(getResponse.status()).toBe(200);

    const retrievedUser = await getResponse.json();
    expect(retrievedUser.id).toBe(createdUser.id);
    expect(retrievedUser.email).toBe("retrieve@example.com");
  });
});

Visual Testing

// tests/visual/homepage.spec.ts
import { test, expect } from "@playwright/test";

test.describe("Visual Regression Tests", () => {
  test("homepage should match visual baseline", async ({ page }) => {
    await page.goto("/");
    await page.waitForLoadState("networkidle");

    // Hide dynamic content that changes between runs
    await page.addStyleTag({
      content: `
        .timestamp, .live-chat, .ads {
          visibility: hidden !important;
        }
      `,
    });

    await expect(page).toHaveScreenshot("homepage.png");
  });

  test("user profile page should match visual baseline", async ({ page }) => {
    // Login first
    await page.goto("/login");
    await page.getByLabel("Email").fill("test@example.com");
    await page.getByLabel("Password").fill("password123");
    await page.getByRole("button", { name: "Sign In" }).click();

    // Navigate to profile
    await page.goto("/profile");
    await page.waitForLoadState("networkidle");

    // Mask dynamic content
    await expect(page).toHaveScreenshot("profile-page.png", {
      mask: [page.getByTestId("last-login-time")],
    });
  });
});

Utility Functions and Helpers

// utils/test-helpers.ts

/**
 * Generate random test data for user creation
 */
export function generateTestUser() {
  const timestamp = Date.now();
  return {
    name: `Test User ${timestamp}`,
    email: `test${timestamp}@example.com`,
    password: "TestPassword123!",
  };
}

/**
 * Wait for element to be stable (not moving/changing)
 */
export async function waitForElementStable(locator: any, timeout = 5000) {
  let previousBoundingBox = await locator.boundingBox();
  const startTime = Date.now();

  while (Date.now() - startTime < timeout) {
    await new Promise((resolve) => setTimeout(resolve, 100));
    const currentBoundingBox = await locator.boundingBox();

    if (
      JSON.stringify(previousBoundingBox) === JSON.stringify(currentBoundingBox)
    ) {
      return;
    }

    previousBoundingBox = currentBoundingBox;
  }

  throw new Error(`Element did not stabilize within ${timeout}ms`);
}

/**
 * Fill form with retry mechanism
 */
export async function fillFormWithRetry(
  locator: any,
  value: string,
  maxRetries = 3
) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await locator.clear();
      await locator.fill(value);

      // Verify the value was set correctly
      const actualValue = await locator.inputValue();
      if (actualValue === value) {
        return;
      }
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }
}

/**
 * Handle file upload with validation
 */
export async function uploadFile(
  page: any,
  fileInputSelector: string,
  filePath: string
) {
  const fileInput = page.locator(fileInputSelector);
  await fileInput.setInputFiles(filePath);

  // Wait for upload to complete
  await page.waitForFunction((selector) => {
    const input = document.querySelector(selector);
    return input && input.files && input.files.length > 0;
  }, fileInputSelector);
}

Best Practices Summary

Test Design and Structure

  • Use descriptive and meaningful test names that clearly describe the expected behavior
  • Utilize Playwright fixtures (test, page, expect) to maintain test isolation and consistency
  • Use test.beforeEach and test.afterEach for setup and teardown to ensure clean state
  • Keep tests DRY by extracting reusable logic into helper functions
  • Focus on critical user paths with tests that are stable, maintainable, and reflect real user behavior

Locator Strategy

  • Avoid page.locator and always use recommended built-in and role-based locators
  • Prefer semantic locators: page.getByRole, page.getByLabel, page.getByText, page.getByTitle
  • Use page.getByTestId whenever data-testid is defined on elements
  • Reuse Playwright locators by using variables or constants for commonly used elements
  • Use web-first assertions (toBeVisible, toHaveText) whenever possible

Configuration and Environment

  • Use playwright.config.ts for global configuration and environment setup
  • Implement proper error handling and logging in tests for clear failure messages
  • Use projects for multiple browsers and devices to ensure cross-browser compatibility
  • Use built-in config objects like devices whenever possible
  • Avoid hardcoded timeouts and use page.waitFor with specific conditions

Performance and Reliability

  • Ensure tests run reliably in parallel without shared state conflicts
  • Use expect matchers for assertions (toEqual, toContain, toBeTruthy, toHaveLength)
  • Avoid assert statements in favor of Playwright's expect matchers
  • Implement retry mechanisms for flaky operations
  • Use proper wait strategies for dynamic content and async operations

Code Quality and Maintenance

  • Add JSDoc comments to describe the purpose of helper functions and reusable logic
  • Avoid commenting on the resulting code unless necessary for complex logic
  • Extract common patterns into reusable page objects and utilities
  • Maintain consistent code style and formatting across test files
  • Follow official Playwright documentation and best practices

CI/CD Integration

  • Configure appropriate retry strategies for CI environments
  • Use proper reporting formats (HTML, JUnit, JSON) for different stakeholders
  • Implement visual regression testing for UI consistency
  • Set up proper artifact collection for screenshots, videos, and traces
  • Use environment-specific configurations for different deployment stages

Security and Data Management

  • Use environment variables for sensitive data like credentials
  • Implement proper test data management with cleanup procedures
  • Avoid hardcoded credentials in test files
  • Use API setup for test data creation when possible
  • Implement proper isolation between test runs and environments

This comprehensive guide provides a solid foundation for building reliable, maintainable end-to-end tests using Playwright with TypeScript and JavaScript, ensuring high-quality QA automation that reflects real user behavior and maintains stability across different browsers and devices.