Back to Testing & Quality Assurance

e2e-testing-patterns

e2e testingplaywrightcypresstest automationqasoftware testingci/cdweb testing
โญ 36.8k๐Ÿ“„ MIT๐Ÿ•’ 2026-06-16Source โ†—

Install this skill

npx skills add wshobson/agents

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

This skill provides architectural frameworks for building predictable browser-based test suites using Playwright. It moves beyond basic scripting by applying structural design patterns like the Page Object Model to decouple test scenarios from underlying DOM changes. By implementing custom fixtures, developers can manage state isolation, authentication requirements, and data persistence lifecycle without bloating individual test files. This approach focuses on user-centric verification, centering on visible interactions rather than brittle implementation details. By standardizing configuration for parallelism and cross-browser coverage, the skill ensures that critical path monitoring remains consistent across local development environments and CI pipelines, reducing maintenance overhead while increasing confidence in the deployment process.

When to Use This Skill

  • โ€ขVerifying authentication and authorization user flows
  • โ€ขTesting multi-step form submissions and data entry processes
  • โ€ขConfirming cross-browser UI consistency for critical application paths
  • โ€ขAutomating regression checks for customer onboarding workflows

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • โ€œCreate a new Playwright test suite for the login flow
  • โ€œRefactor my browser tests to use the Page Object Model
  • โ€œSet up custom fixtures to handle database test users
  • โ€œConfigure playwright.config.ts for parallel CI execution
  • โ€œDebug a flaky test using browser trace viewer

Pro Tips

  • ๐Ÿ’กPrioritize critical user flows for E2E tests to keep them fast and maintainable, reserving unit and integration tests for lower-level logic.
  • ๐Ÿ’กLeverage custom commands, page object models, and data-driven testing to improve test readability, reduce duplication, and simplify maintenance.
  • ๐Ÿ’กIntegrate visual regression testing into your E2E suite to automatically catch unintended UI changes across different browsers and viewports.

What this skill does

  • โ€ขModularize test code with the Page Object Model pattern
  • โ€ขDefine lifecycle-managed test fixtures for dynamic state
  • โ€ขImplement cross-browser execution across Chromium, Firefox, and WebKit
  • โ€ขConfigure parallel test execution and automatic retry logic
  • โ€ขGenerate visual artifacts like trace files and screenshots for debugging failures

When not to use it

  • โœ•Validating internal function logic or isolated component state
  • โœ•Executing high-volume performance load testing
  • โœ•Testing API-only endpoints that lack a user interface

Example workflow

  1. Define a base Page class that maps UI elements to semantic methods
  2. Create a custom test fixture to handle dynamic account creation and cleanup
  3. Write a test case asserting the redirection to the dashboard after login
  4. Configure project-specific browsers in the Playwright config file
  5. Run the test suite in headless mode to verify success

Prerequisites

  • โ€“Existing Playwright installation
  • โ€“Node.js environment
  • โ€“Basic knowledge of TypeScript

Pitfalls & limitations

  • !Over-testing edge cases that are better served by unit tests
  • !Writing non-deterministic tests that depend on external volatile data
  • !Tight coupling tests to specific CSS selectors instead of accessibility roles

FAQ

Why use Page Objects instead of raw Playwright selectors?
Page Objects centralize element locators in one place, making them easier to update if your UI structure changes.
How do I prevent data pollution during testing?
Use custom test fixtures to handle both the creation of test data before a test and the deletion of that data after the test concludes.
Is it necessary to test on all three major browsers?
While not always required, testing across Chromium, Firefox, and WebKit catches layout or engine-specific bugs that unit tests cannot detect.

How it compares

Unlike manual testing or ad-hoc browser automation scripts, this approach emphasizes architectural patterns that prevent code duplication and ensure long-term test maintainability.

Source & trust

โญ 37k stars๐Ÿ“„ MIT๐Ÿ•’ Updated 2026-06-16
๐Ÿ“„ Full skill instructions โ€” original source: wshobson/agents
# E2E Testing Patterns

Build reliable, fast, and maintainable end-to-end test suites that provide confidence to ship code quickly and catch regressions before users do.

## When to Use This Skill

- Implementing end-to-end test automation
- Debugging flaky or unreliable tests
- Testing critical user workflows
- Setting up CI/CD test pipelines
- Testing across multiple browsers
- Validating accessibility requirements
- Testing responsive designs
- Establishing E2E testing standards

## Core Concepts

### 1. E2E Testing Fundamentals

**What to Test with E2E:**

- Critical user journeys (login, checkout, signup)
- Complex interactions (drag-and-drop, multi-step forms)
- Cross-browser compatibility
- Real API integration
- Authentication flows

**What NOT to Test with E2E:**

- Unit-level logic (use unit tests)
- API contracts (use integration tests)
- Edge cases (too slow)
- Internal implementation details

### 2. Test Philosophy

**The Testing Pyramid:**

/\
/E2E\ โ† Few, focused on critical paths
/โ”€โ”€โ”€โ”€โ”€\
/Integr\ โ† More, test component interactions
/โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\
/Unit Tests\ โ† Many, fast, isolated
/โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\


**Best Practices:**

- Test user behavior, not implementation
- Keep tests independent
- Make tests deterministic
- Optimize for speed
- Use data-testid, not CSS selectors

## Playwright Patterns

### Setup and Configuration

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

export default defineConfig({
testDir: "./e2e",
timeout: 30000,
expect: {
timeout: 5000,
},
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [["html"], ["junit", { outputFile: "results.xml" }]],
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
{ name: "mobile", use: { ...devices["iPhone 13"] } },
],
});


### Pattern 1: Page Object Model

// pages/LoginPage.ts
import { Page, Locator } from "@playwright/test";

export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;

constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.loginButton = page.getByRole("button", { name: "Login" });
this.errorMessage = page.getByRole("alert");
}

async goto() {
await this.page.goto("/login");
}

async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}

async getErrorMessage(): Promise<string> {
return (await this.errorMessage.textContent()) ?? "";
}
}

// Test using Page Object
import { test, expect } from "@playwright/test";
import { LoginPage } from "./pages/LoginPage";

test("successful login", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("[email protected]", "password123");

await expect(page).toHaveURL("/dashboard");
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
});

test("failed login shows error", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("[email protected]", "wrong");

const error = await loginPage.getErrorMessage();
expect(error).toContain("Invalid credentials");
});


### Pattern 2: Fixtures for Test Data

// fixtures/test-data.ts
import { test as base } from "@playwright/test";

type TestData = {
testUser: {
email: string;
password: string;
name: string;
};
adminUser: {
email: string;
password: string;
};
};

export const test = base.extend<TestData>({
testUser: async ({}, use) => {
const user = {
email: test-${Date.now()}@example.com,
password: "Test123!@#",
name: "Test User",
};
// Setup: Create user in database
await createTestUser(user);
await use(user);
// Teardown: Clean up user
await deleteTestUser(user.email);
},

adminUser: async ({}, use) => {
await use({
email: "[email protected]",
password: process.env.ADMIN_PASSWORD!,
});
},
});

// Usage in tests
import { test } from "./fixtures/test-data";

test("user can update profile", async ({ page, testUser }) => {
await page.goto("/login");
await page.getByLabel("Email").fill(testUser.email);
await page.getByLabel("Password").fill(testUser.password);
await page.getByRole("button", { name: "Login" }).click();

await page.goto("/profile");
await page.getByLabel("Name").fill("Updated Name");
await page.getByRole("button", { name: "Save" }).click();

await expect(page.getByText("Profile updated")).toBeVisible();
});


### Pattern 3: Waiting Strategies

// โŒ Bad: Fixed timeouts
await page.waitForTimeout(3000); // Flaky!

// โœ… Good: Wait for specific conditions
await page.waitForLoadState("networkidle");
await page.waitForURL("/dashboard");
await page.waitForSelector('[data-testid="user-profile"]');

// โœ… Better: Auto-waiting with assertions
await expect(page.getByText("Welcome")).toBeVisible();
await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled();

// Wait for API response
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes("/api/users") && response.status() === 200,
);
await page.getByRole("button", { name: "Load Users" }).click();
const response = await responsePromise;
const data = await response.json();
expect(data.users).toHaveLength(10);

// Wait for multiple conditions
await Promise.all([
page.waitForURL("/success"),
page.waitForLoadState("networkidle"),
expect(page.getByText("Payment successful")).toBeVisible(),
]);


### Pattern 4: Network Mocking and Interception

// Mock API responses
test("displays error when API fails", async ({ page }) => {
await page.route("**/api/users", (route) => {
route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: "Internal Server Error" }),
});
});

await page.goto("/users");
await expect(page.getByText("Failed to load users")).toBeVisible();
});

// Intercept and modify requests
test("can modify API request", async ({ page }) => {
await page.route("**/api/users", async (route) => {
const request = route.request();
const postData = JSON.parse(request.postData() || "{}");

// Modify request
postData.role = "admin";

await route.continue({
postData: JSON.stringify(postData),
});
});

// Test continues...
});

// Mock third-party services
test("payment flow with mocked Stripe", async ({ page }) => {
await page.route("**/api/stripe/**", (route) => {
route.fulfill({
status: 200,
body: JSON.stringify({
id: "mock_payment_id",
status: "succeeded",
}),
});
});

// Test payment flow with mocked response
});


## Cypress Patterns

### Setup and Configuration

// cypress.config.ts
import { defineConfig } from "cypress";

export default defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
viewportWidth: 1280,
viewportHeight: 720,
video: false,
screenshotOnRunFailure: true,
defaultCommandTimeout: 10000,
requestTimeout: 10000,
setupNodeEvents(on, config) {
// Implement node event listeners
},
},
});


### Pattern 1: Custom Commands

// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
createUser(userData: UserData): Chainable<User>;
dataCy(value: string): Chainable<JQuery<HTMLElement>>;
}
}
}

Cypress.Commands.add("login", (email: string, password: string) => {
cy.visit("/login");
cy.get('[data-testid="email"]').type(email);
cy.get('[data-testid="password"]').type(password);
cy.get('[data-testid="login-button"]').click();
cy.url().should("include", "/dashboard");
});

Cypress.Commands.add("createUser", (userData: UserData) => {
return cy.request("POST", "/api/users", userData).its("body");
});

Cypress.Commands.add("dataCy", (value: string) => {
return cy.get([data-cy="${value}"]);
});

// Usage
cy.login("[email protected]", "password");
cy.dataCy("submit-button").click();


### Pattern 2: Cypress Intercept

// Mock API calls
cy.intercept("GET", "/api/users", {
statusCode: 200,
body: [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
],
}).as("getUsers");

cy.visit("/users");
cy.wait("@getUsers");
cy.get('[data-testid="user-list"]').children().should("have.length", 2);

// Modify responses
cy.intercept("GET", "/api/users", (req) => {
req.reply((res) => {
// Modify response
res.body.users = res.body.users.slice(0, 5);
res.send();
});
});

// Simulate slow network
cy.intercept("GET", "/api/data", (req) => {
req.reply((res) => {
res.delay(3000); // 3 second delay
res.send();
});
});


## Advanced Patterns

### Pattern 1: Visual Regression Testing

// With Playwright
import { test, expect } from "@playwright/test";

test("homepage looks correct", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveScreenshot("homepage.png", {
fullPage: true,
maxDiffPixels: 100,
});
});

test("button in all states", async ({ page }) => {
await page.goto("/components");

const button = page.getByRole("button", { name: "Submit" });

// Default state
await expect(button).toHaveScreenshot("button-default.png");

// Hover state
await button.hover();
await expect(button).toHaveScreenshot("button-hover.png");

// Disabled state
await button.evaluate((el) => el.setAttribute("disabled", "true"));
await expect(button).toHaveScreenshot("button-disabled.png");
});


### Pattern 2: Parallel Testing with Sharding

// playwright.config.ts
export default defineConfig({
projects: [
{
name: "shard-1",
use: { ...devices["Desktop Chrome"] },
grepInvert: /@slow/,
shard: { current: 1, total: 4 },
},
{
name: "shard-2",
use: { ...devices["Desktop Chrome"] },
shard: { current: 2, total: 4 },
},
// ... more shards
],
});

// Run in CI
// npx playwright test --shard=1/4
// npx playwright test --shard=2/4


### Pattern 3: Accessibility Testing

// Install: npm install @axe-core/playwright
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

test("page should not have accessibility violations", async ({ page }) => {
await page.goto("/");

const accessibilityScanResults = await new AxeBuilder({ page })
.exclude("#third-party-widget")
.analyze();

expect(accessibilityScanResults.violations).toEqual([]);
});

test("form is accessible", async ({ page }) => {
await page.goto("/signup");

const results = await new AxeBuilder({ page }).include("form").analyze();

expect(results.violations).toEqual([]);
});


## Best Practices

1. **Use Data Attributes**: data-testid or data-cy for stable selectors
2. **Avoid Brittle Selectors**: Don't rely on CSS classes or DOM structure
3. **Test User Behavior**: Click, type, see - not implementation details
4. **Keep Tests Independent**: Each test should run in isolation
5. **Clean Up Test Data**: Create and destroy test data in each test
6. **Use Page Objects**: Encapsulate page logic
7. **Meaningful Assertions**: Check actual user-visible behavior
8. **Optimize for Speed**: Mock when possible, parallel execution

// โŒ Bad selectors
cy.get(".btn.btn-primary.submit-button").click();
cy.get("div > form > div:nth-child(2) > input").type("text");

// โœ… Good selectors
cy.getByRole("button", { name: "Submit" }).click();
cy.getByLabel("Email address").type("[email protected]");
cy.get('[data-testid="email-input"]').type("[email protected]");


## Common Pitfalls

- **Flaky Tests**: Use proper waits, not fixed timeouts
- **Slow Tests**: Mock external APIs, use parallel execution
- **Over-Testing**: Don't test every edge case with E2E
- **Coupled Tests**: Tests should not depend on each other
- **Poor Selectors**: Avoid CSS classes and nth-child
- **No Cleanup**: Clean up test data after each test
- **Testing Implementation**: Test user behavior, not internals

## Debugging Failing Tests

// Playwright debugging
// 1. Run in headed mode
npx playwright test --headed

// 2. Run in debug mode
npx playwright test --debug

// 3. Use trace viewer
await page.screenshot({ path: 'screenshot.png' });
await page.video()?.saveAs('video.webm');

// 4. Add test.step for better reporting
test('checkout flow', async ({ page }) => {
await test.step('Add item to cart', async () => {
await page.goto('/products');
await page.getByRole('button', { name: 'Add to Cart' }).click();
});

await test.step('Proceed to checkout', async () => {
await page.goto('/cart');
await page.getByRole('button', { name: 'Checkout' }).click();
});
});

// 5. Inspect page state
await page.pause(); // Pauses execution, opens inspector


## Resources

- **references/playwright-best-practices.md**: Playwright-specific patterns
- **references/cypress-best-practices.md**: Cypress-specific patterns
- **references/flaky-test-debugging.md**: Debugging unreliable tests
- **assets/e2e-testing-checklist.md**: What to test with E2E
- **assets/selector-strategies.md**: Finding reliable selectors
- **scripts/test-analyzer.ts**: Analyze test flakiness and duration

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/e2e-testing-patterns/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/wshobson/agents/e2e-testing-patterns/SKILL.md
  • Cursor: ~/.cursor/skills/wshobson/agents/e2e-testing-patterns/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/wshobson/agents/e2e-testing-patterns/SKILL.md

๐Ÿš€ Install with CLI:
npx skills add wshobson/agents

Read the Master Guide: Mastering Agent Skills โ†’

Recommended Rules

View more rules โ†’

Recommended Workflows

View more workflows โ†’

Recommended MCP Servers

View more MCP servers โ†’

Take It Further

Maximize your productivity with these powerful resources

๐Ÿ“‹

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
๐Ÿ“–

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid testing & quality assurance issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Testing & Quality Assurance and is published by W. Shobson, maintained in wshobson/agents.

โ† Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.