Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Implement end-to-end testing patterns across frameworks with proper test structure, data setup, and CI integration.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
SKILL.md
1---2name: e2e-testing-patterns3description: Master end-to-end testing with Playwright and Cypress to build reliable test suites that catch bugs, improve confidence, and enable fast deployment. Use when implementing E2E tests, debugging flaky tests, or establishing testing standards.4---56# E2E Testing Patterns78Build reliable, fast, and maintainable end-to-end test suites that provide confidence to ship code quickly and catch regressions before users do.910## When to Use This Skill1112- Implementing end-to-end test automation13- Debugging flaky or unreliable tests14- Testing critical user workflows15- Setting up CI/CD test pipelines16- Testing across multiple browsers17- Validating accessibility requirements18- Testing responsive designs19- Establishing E2E testing standards2021## Core Concepts2223### 1. E2E Testing Fundamentals2425**What to Test with E2E:**2627- Critical user journeys (login, checkout, signup)28- Complex interactions (drag-and-drop, multi-step forms)29- Cross-browser compatibility30- Real API integration31- Authentication flows3233**What NOT to Test with E2E:**3435- Unit-level logic (use unit tests)36- API contracts (use integration tests)37- Edge cases (too slow)38- Internal implementation details3940### 2. Test Philosophy4142**The Testing Pyramid:**4344```45/\46/E2E\ ← Few, focused on critical paths47/─────\48/Integr\ ← More, test component interactions49/────────\50/Unit Tests\ ← Many, fast, isolated51/────────────\52```5354**Best Practices:**5556- Test user behavior, not implementation57- Keep tests independent58- Make tests deterministic59- Optimize for speed60- Use data-testid, not CSS selectors6162## Playwright Patterns6364### Setup and Configuration6566```typescript67// playwright.config.ts68import { defineConfig, devices } from "@playwright/test";6970export default defineConfig({71testDir: "./e2e",72timeout: 30000,73expect: {74timeout: 5000,75},76fullyParallel: true,77forbidOnly: !!process.env.CI,78retries: process.env.CI ? 2 : 0,79workers: process.env.CI ? 1 : undefined,80reporter: [["html"], ["junit", { outputFile: "results.xml" }]],81use: {82baseURL: "http://localhost:3000",83trace: "on-first-retry",84screenshot: "only-on-failure",85video: "retain-on-failure",86},87projects: [88{ name: "chromium", use: { ...devices["Desktop Chrome"] } },89{ name: "firefox", use: { ...devices["Desktop Firefox"] } },90{ name: "webkit", use: { ...devices["Desktop Safari"] } },91{ name: "mobile", use: { ...devices["iPhone 13"] } },92],93});94```9596### Pattern 1: Page Object Model9798```typescript99// pages/LoginPage.ts100import { Page, Locator } from "@playwright/test";101102export class LoginPage {103readonly page: Page;104readonly emailInput: Locator;105readonly passwordInput: Locator;106readonly loginButton: Locator;107readonly errorMessage: Locator;108109constructor(page: Page) {110this.page = page;111this.emailInput = page.getByLabel("Email");112this.passwordInput = page.getByLabel("Password");113this.loginButton = page.getByRole("button", { name: "Login" });114this.errorMessage = page.getByRole("alert");115}116117async goto() {118await this.page.goto("/login");119}120121async login(email: string, password: string) {122await this.emailInput.fill(email);123await this.passwordInput.fill(password);124await this.loginButton.click();125}126127async getErrorMessage(): Promise<string> {128return (await this.errorMessage.textContent()) ?? "";129}130}131132// Test using Page Object133import { test, expect } from "@playwright/test";134import { LoginPage } from "./pages/LoginPage";135136test("successful login", async ({ page }) => {137const loginPage = new LoginPage(page);138await loginPage.goto();139await loginPage.login("[email protected]", "password123");140141await expect(page).toHaveURL("/dashboard");142await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();143});144145test("failed login shows error", async ({ page }) => {146const loginPage = new LoginPage(page);147await loginPage.goto();148await loginPage.login("[email protected]", "wrong");149150const error = await loginPage.getErrorMessage();151expect(error).toContain("Invalid credentials");152});153```154155### Pattern 2: Fixtures for Test Data156157```typescript158// fixtures/test-data.ts159import { test as base } from "@playwright/test";160161type TestData = {162testUser: {163email: string;164password: string;165name: string;166};167adminUser: {168email: string;169password: string;170};171};172173export const test = base.extend<TestData>({174testUser: async ({}, use) => {175const user = {176email: `test-${Date.now()}@example.com`,177password: "Test123!@#",178name: "Test User",179};180// Setup: Create user in database181await createTestUser(user);182await use(user);183// Teardown: Clean up user184await deleteTestUser(user.email);185},186187adminUser: async ({}, use) => {188await use({189email: "[email protected]",190password: process.env.ADMIN_PASSWORD!,191});192},193});194195// Usage in tests196import { test } from "./fixtures/test-data";197198test("user can update profile", async ({ page, testUser }) => {199await page.goto("/login");200await page.getByLabel("Email").fill(testUser.email);201await page.getByLabel("Password").fill(testUser.password);202await page.getByRole("button", { name: "Login" }).click();203204await page.goto("/profile");205await page.getByLabel("Name").fill("Updated Name");206await page.getByRole("button", { name: "Save" }).click();207208await expect(page.getByText("Profile updated")).toBeVisible();209});210```211212### Pattern 3: Waiting Strategies213214```typescript215// ❌ Bad: Fixed timeouts216await page.waitForTimeout(3000); // Flaky!217218// ✅ Good: Wait for specific conditions219await page.waitForLoadState("networkidle");220await page.waitForURL("/dashboard");221await page.waitForSelector('[data-testid="user-profile"]');222223// ✅ Better: Auto-waiting with assertions224await expect(page.getByText("Welcome")).toBeVisible();225await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled();226227// Wait for API response228const responsePromise = page.waitForResponse(229(response) =>230response.url().includes("/api/users") && response.status() === 200,231);232await page.getByRole("button", { name: "Load Users" }).click();233const response = await responsePromise;234const data = await response.json();235expect(data.users).toHaveLength(10);236237// Wait for multiple conditions238await Promise.all([239page.waitForURL("/success"),240page.waitForLoadState("networkidle"),241expect(page.getByText("Payment successful")).toBeVisible(),242]);243```244245### Pattern 4: Network Mocking and Interception246247```typescript248// Mock API responses249test("displays error when API fails", async ({ page }) => {250await page.route("**/api/users", (route) => {251route.fulfill({252status: 500,253contentType: "application/json",254body: JSON.stringify({ error: "Internal Server Error" }),255});256});257258await page.goto("/users");259await expect(page.getByText("Failed to load users")).toBeVisible();260});261262// Intercept and modify requests263test("can modify API request", async ({ page }) => {264await page.route("**/api/users", async (route) => {265const request = route.request();266const postData = JSON.parse(request.postData() || "{}");267268// Modify request269postData.role = "admin";270271await route.continue({272postData: JSON.stringify(postData),273});274});275276// Test continues...277});278279// Mock third-party services280test("payment flow with mocked Stripe", async ({ page }) => {281await page.route("**/api/stripe/**", (route) => {282route.fulfill({283status: 200,284body: JSON.stringify({285id: "mock_payment_id",286status: "succeeded",287}),288});289});290291// Test payment flow with mocked response292});293```294295## Cypress Patterns296297### Setup and Configuration298299```typescript300// cypress.config.ts301import { defineConfig } from "cypress";302303export default defineConfig({304e2e: {305baseUrl: "http://localhost:3000",306viewportWidth: 1280,307viewportHeight: 720,308video: false,309screenshotOnRunFailure: true,310defaultCommandTimeout: 10000,311requestTimeout: 10000,312setupNodeEvents(on, config) {313// Implement node event listeners314},315},316});317```318319### Pattern 1: Custom Commands320321```typescript322// cypress/support/commands.ts323declare global {324namespace Cypress {325interface Chainable {326login(email: string, password: string): Chainable<void>;327createUser(userData: UserData): Chainable<User>;328dataCy(value: string): Chainable<JQuery<HTMLElement>>;329}330}331}332333Cypress.Commands.add("login", (email: string, password: string) => {334cy.visit("/login");335cy.get('[data-testid="email"]').type(email);336cy.get('[data-testid="password"]').type(password);337cy.get('[data-testid="login-button"]').click();338cy.url().should("include", "/dashboard");339});340341Cypress.Commands.add("createUser", (userData: UserData) => {342return cy.request("POST", "/api/users", userData).its("body");343});344345Cypress.Commands.add("dataCy", (value: string) => {346return cy.get(`[data-cy="${value}"]`);347});348349// Usage350cy.login("[email protected]", "password");351cy.dataCy("submit-button").click();352```353354### Pattern 2: Cypress Intercept355356```typescript357// Mock API calls358cy.intercept("GET", "/api/users", {359statusCode: 200,360body: [361{ id: 1, name: "John" },362{ id: 2, name: "Jane" },363],364}).as("getUsers");365366cy.visit("/users");367cy.wait("@getUsers");368cy.get('[data-testid="user-list"]').children().should("have.length", 2);369370// Modify responses371cy.intercept("GET", "/api/users", (req) => {372req.reply((res) => {373// Modify response374res.body.users = res.body.users.slice(0, 5);375res.send();376});377});378379// Simulate slow network380cy.intercept("GET", "/api/data", (req) => {381req.reply((res) => {382res.delay(3000); // 3 second delay383res.send();384});385});386```387388## Advanced Patterns389390### Pattern 1: Visual Regression Testing391392```typescript393// With Playwright394import { test, expect } from "@playwright/test";395396test("homepage looks correct", async ({ page }) => {397await page.goto("/");398await expect(page).toHaveScreenshot("homepage.png", {399fullPage: true,400maxDiffPixels: 100,401});402});403404test("button in all states", async ({ page }) => {405await page.goto("/components");406407const button = page.getByRole("button", { name: "Submit" });408409// Default state410await expect(button).toHaveScreenshot("button-default.png");411412// Hover state413await button.hover();414await expect(button).toHaveScreenshot("button-hover.png");415416// Disabled state417await button.evaluate((el) => el.setAttribute("disabled", "true"));418await expect(button).toHaveScreenshot("button-disabled.png");419});420```421422### Pattern 2: Parallel Testing with Sharding423424```typescript425// playwright.config.ts426export default defineConfig({427projects: [428{429name: "shard-1",430use: { ...devices["Desktop Chrome"] },431grepInvert: /@slow/,432shard: { current: 1, total: 4 },433},434{435name: "shard-2",436use: { ...devices["Desktop Chrome"] },437shard: { current: 2, total: 4 },438},439// ... more shards440],441});442443// Run in CI444// npx playwright test --shard=1/4445// npx playwright test --shard=2/4446```447448### Pattern 3: Accessibility Testing449450```typescript451// Install: npm install @axe-core/playwright452import { test, expect } from "@playwright/test";453import AxeBuilder from "@axe-core/playwright";454455test("page should not have accessibility violations", async ({ page }) => {456await page.goto("/");457458const accessibilityScanResults = await new AxeBuilder({ page })459.exclude("#third-party-widget")460.analyze();461462expect(accessibilityScanResults.violations).toEqual([]);463});464465test("form is accessible", async ({ page }) => {466await page.goto("/signup");467468const results = await new AxeBuilder({ page }).include("form").analyze();469470expect(results.violations).toEqual([]);471});472```473474## Best Practices4754761. **Use Data Attributes**: `data-testid` or `data-cy` for stable selectors4772. **Avoid Brittle Selectors**: Don't rely on CSS classes or DOM structure4783. **Test User Behavior**: Click, type, see - not implementation details4794. **Keep Tests Independent**: Each test should run in isolation4805. **Clean Up Test Data**: Create and destroy test data in each test4816. **Use Page Objects**: Encapsulate page logic4827. **Meaningful Assertions**: Check actual user-visible behavior4838. **Optimize for Speed**: Mock when possible, parallel execution484485```typescript486// ❌ Bad selectors487cy.get(".btn.btn-primary.submit-button").click();488cy.get("div > form > div:nth-child(2) > input").type("text");489490// ✅ Good selectors491cy.getByRole("button", { name: "Submit" }).click();492cy.getByLabel("Email address").type("[email protected]");493cy.get('[data-testid="email-input"]').type("[email protected]");494```495496## Common Pitfalls497498- **Flaky Tests**: Use proper waits, not fixed timeouts499- **Slow Tests**: Mock external APIs, use parallel execution500- **Over-Testing**: Don't test every edge case with E2E501- **Coupled Tests**: Tests should not depend on each other502- **Poor Selectors**: Avoid CSS classes and nth-child503- **No Cleanup**: Clean up test data after each test504- **Testing Implementation**: Test user behavior, not internals505506## Debugging Failing Tests507508```typescript509// Playwright debugging510// 1. Run in headed mode511npx playwright test --headed512513// 2. Run in debug mode514npx playwright test --debug515516// 3. Use trace viewer517await page.screenshot({ path: 'screenshot.png' });518await page.video()?.saveAs('video.webm');519520// 4. Add test.step for better reporting521test('checkout flow', async ({ page }) => {522await test.step('Add item to cart', async () => {523await page.goto('/products');524await page.getByRole('button', { name: 'Add to Cart' }).click();525});526527await test.step('Proceed to checkout', async () => {528await page.goto('/cart');529await page.getByRole('button', { name: 'Checkout' }).click();530});531});532533// 5. Inspect page state534await page.pause(); // Pauses execution, opens inspector535```536