Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    wshobson

    e2e-testing-patterns

    wshobson/e2e-testing-patterns
    Coding
    28,185
    2 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Master end-to-end testing with Playwright and Cypress to build reliable test suites that catch bugs, improve confidence, and enable fast deployment...

    SKILL.md

    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("user@example.com", "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("invalid@example.com", "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: "admin@example.com",
          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("user@example.com", "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("user@example.com");
    cy.get('[data-testid="email-input"]').type("user@example.com");
    

    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
    
    Recommended Servers
    Postman
    Postman
    Vercel
    Vercel
    OpenZeppelin
    OpenZeppelin
    Repository
    wshobson/agents
    Files