Agents.md LogoAgents.md

Playwright End-to-End Testing Automation Guide - Agents.md Example

A comprehensive guide for QA automation engineers using Playwright with TypeScript and JavaScript for reliable end-to-end testing across web applications

Last Updated:Jun 24, 2025
Tags:
playwrighte2e-testingtypescriptjavascriptqa-automationtestingweb-testingcross-browser
View on GitHub

About This Agents.md Example

Key Features of This Agents.md Example

This agents.md example showcases industry best practices for AI agent instruction. The agents.md example provides a comprehensive template that you can adapt for your specific project requirements.

Every element in this agents.md example has been carefully designed to optimize OpenAI Codex performance and ensure consistent AI agent behavior across your development workflow.

How to Implement This Agents.md Example

To use this agents.md example in your project, download the template and customize it according to your specific needs. This agents.md example serves as a solid foundation for your AI agent configuration.

Study the structure and conventions used in this agents.md example to understand how successful projects implement AI agent instruction for optimal results.

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(
      "[email protected]",
      "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("[email protected]");
    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 || "[email protected]";
    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: "[email protected]",
      password: "password123",
    },
    adminUser: {
      email: "[email protected]",
      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: "[email protected]",
      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: "[email protected]",
        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("[email protected]");
  });
});

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("[email protected]");
    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.