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.
references/details.md
1# e2e-testing-patterns — detailed patterns and worked examples23## Playwright Patterns45### Setup and Configuration67```typescript8// playwright.config.ts9import { defineConfig, devices } from "@playwright/test";1011export default defineConfig({12testDir: "./e2e",13timeout: 30000,14expect: {15timeout: 5000,16},17fullyParallel: true,18forbidOnly: !!process.env.CI,19retries: process.env.CI ? 2 : 0,20workers: process.env.CI ? 1 : undefined,21reporter: [["html"], ["junit", { outputFile: "results.xml" }]],22use: {23baseURL: "http://localhost:3000",24trace: "on-first-retry",25screenshot: "only-on-failure",26video: "retain-on-failure",27},28projects: [29{ name: "chromium", use: { ...devices["Desktop Chrome"] } },30{ name: "firefox", use: { ...devices["Desktop Firefox"] } },31{ name: "webkit", use: { ...devices["Desktop Safari"] } },32{ name: "mobile", use: { ...devices["iPhone 13"] } },33],34});35```3637### Pattern 1: Page Object Model3839```typescript40// pages/LoginPage.ts41import { Page, Locator } from "@playwright/test";4243export class LoginPage {44readonly page: Page;45readonly emailInput: Locator;46readonly passwordInput: Locator;47readonly loginButton: Locator;48readonly errorMessage: Locator;4950constructor(page: Page) {51this.page = page;52this.emailInput = page.getByLabel("Email");53this.passwordInput = page.getByLabel("Password");54this.loginButton = page.getByRole("button", { name: "Login" });55this.errorMessage = page.getByRole("alert");56}5758async goto() {59await this.page.goto("/login");60}6162async login(email: string, password: string) {63await this.emailInput.fill(email);64await this.passwordInput.fill(password);65await this.loginButton.click();66}6768async getErrorMessage(): Promise<string> {69return (await this.errorMessage.textContent()) ?? "";70}71}7273// Test using Page Object74import { test, expect } from "@playwright/test";75import { LoginPage } from "./pages/LoginPage";7677test("successful login", async ({ page }) => {78const loginPage = new LoginPage(page);79await loginPage.goto();80await loginPage.login("[email protected]", "password123");8182await expect(page).toHaveURL("/dashboard");83await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();84});8586test("failed login shows error", async ({ page }) => {87const loginPage = new LoginPage(page);88await loginPage.goto();89await loginPage.login("[email protected]", "wrong");9091const error = await loginPage.getErrorMessage();92expect(error).toContain("Invalid credentials");93});94```9596### Pattern 2: Fixtures for Test Data9798```typescript99// fixtures/test-data.ts100import { test as base } from "@playwright/test";101102type TestData = {103testUser: {104email: string;105password: string;106name: string;107};108adminUser: {109email: string;110password: string;111};112};113114export const test = base.extend<TestData>({115testUser: async ({}, use) => {116const user = {117email: `test-${Date.now()}@example.com`,118password: "Test123!@#",119name: "Test User",120};121// Setup: Create user in database122await createTestUser(user);123await use(user);124// Teardown: Clean up user125await deleteTestUser(user.email);126},127128adminUser: async ({}, use) => {129await use({130email: "[email protected]",131password: process.env.ADMIN_PASSWORD!,132});133},134});135136// Usage in tests137import { test } from "./fixtures/test-data";138139test("user can update profile", async ({ page, testUser }) => {140await page.goto("/login");141await page.getByLabel("Email").fill(testUser.email);142await page.getByLabel("Password").fill(testUser.password);143await page.getByRole("button", { name: "Login" }).click();144145await page.goto("/profile");146await page.getByLabel("Name").fill("Updated Name");147await page.getByRole("button", { name: "Save" }).click();148149await expect(page.getByText("Profile updated")).toBeVisible();150});151```152153### Pattern 3: Waiting Strategies154155```typescript156// ❌ Bad: Fixed timeouts157await page.waitForTimeout(3000); // Flaky!158159// ✅ Good: Wait for specific conditions160await page.waitForLoadState("networkidle");161await page.waitForURL("/dashboard");162await page.waitForSelector('[data-testid="user-profile"]');163164// ✅ Better: Auto-waiting with assertions165await expect(page.getByText("Welcome")).toBeVisible();166await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled();167168// Wait for API response169const responsePromise = page.waitForResponse(170(response) =>171response.url().includes("/api/users") && response.status() === 200,172);173await page.getByRole("button", { name: "Load Users" }).click();174const response = await responsePromise;175const data = await response.json();176expect(data.users).toHaveLength(10);177178// Wait for multiple conditions179await Promise.all([180page.waitForURL("/success"),181page.waitForLoadState("networkidle"),182expect(page.getByText("Payment successful")).toBeVisible(),183]);184```185186### Pattern 4: Network Mocking and Interception187188```typescript189// Mock API responses190test("displays error when API fails", async ({ page }) => {191await page.route("**/api/users", (route) => {192route.fulfill({193status: 500,194contentType: "application/json",195body: JSON.stringify({ error: "Internal Server Error" }),196});197});198199await page.goto("/users");200await expect(page.getByText("Failed to load users")).toBeVisible();201});202203// Intercept and modify requests204test("can modify API request", async ({ page }) => {205await page.route("**/api/users", async (route) => {206const request = route.request();207const postData = JSON.parse(request.postData() || "{}");208209// Modify request210postData.role = "admin";211212await route.continue({213postData: JSON.stringify(postData),214});215});216217// Test continues...218});219220// Mock third-party services221test("payment flow with mocked Stripe", async ({ page }) => {222await page.route("**/api/stripe/**", (route) => {223route.fulfill({224status: 200,225body: JSON.stringify({226id: "mock_payment_id",227status: "succeeded",228}),229});230});231232// Test payment flow with mocked response233});234```235236## Cypress Patterns237238### Setup and Configuration239240```typescript241// cypress.config.ts242import { defineConfig } from "cypress";243244export default defineConfig({245e2e: {246baseUrl: "http://localhost:3000",247viewportWidth: 1280,248viewportHeight: 720,249video: false,250screenshotOnRunFailure: true,251defaultCommandTimeout: 10000,252requestTimeout: 10000,253setupNodeEvents(on, config) {254// Implement node event listeners255},256},257});258```259260### Pattern 1: Custom Commands261262```typescript263// cypress/support/commands.ts264declare global {265namespace Cypress {266interface Chainable {267login(email: string, password: string): Chainable<void>;268createUser(userData: UserData): Chainable<User>;269dataCy(value: string): Chainable<JQuery<HTMLElement>>;270}271}272}273274Cypress.Commands.add("login", (email: string, password: string) => {275cy.visit("/login");276cy.get('[data-testid="email"]').type(email);277cy.get('[data-testid="password"]').type(password);278cy.get('[data-testid="login-button"]').click();279cy.url().should("include", "/dashboard");280});281282Cypress.Commands.add("createUser", (userData: UserData) => {283return cy.request("POST", "/api/users", userData).its("body");284});285286Cypress.Commands.add("dataCy", (value: string) => {287return cy.get(`[data-cy="${value}"]`);288});289290// Usage291cy.login("[email protected]", "password");292cy.dataCy("submit-button").click();293```294295### Pattern 2: Cypress Intercept296297```typescript298// Mock API calls299cy.intercept("GET", "/api/users", {300statusCode: 200,301body: [302{ id: 1, name: "John" },303{ id: 2, name: "Jane" },304],305}).as("getUsers");306307cy.visit("/users");308cy.wait("@getUsers");309cy.get('[data-testid="user-list"]').children().should("have.length", 2);310311// Modify responses312cy.intercept("GET", "/api/users", (req) => {313req.reply((res) => {314// Modify response315res.body.users = res.body.users.slice(0, 5);316res.send();317});318});319320// Simulate slow network321cy.intercept("GET", "/api/data", (req) => {322req.reply((res) => {323res.delay(3000); // 3 second delay324res.send();325});326});327```328329## Advanced Patterns330331### Pattern 1: Visual Regression Testing332333```typescript334// With Playwright335import { test, expect } from "@playwright/test";336337test("homepage looks correct", async ({ page }) => {338await page.goto("/");339await expect(page).toHaveScreenshot("homepage.png", {340fullPage: true,341maxDiffPixels: 100,342});343});344345test("button in all states", async ({ page }) => {346await page.goto("/components");347348const button = page.getByRole("button", { name: "Submit" });349350// Default state351await expect(button).toHaveScreenshot("button-default.png");352353// Hover state354await button.hover();355await expect(button).toHaveScreenshot("button-hover.png");356357// Disabled state358await button.evaluate((el) => el.setAttribute("disabled", "true"));359await expect(button).toHaveScreenshot("button-disabled.png");360});361```362363### Pattern 2: Parallel Testing with Sharding364365```typescript366// playwright.config.ts367export default defineConfig({368projects: [369{370name: "shard-1",371use: { ...devices["Desktop Chrome"] },372grepInvert: /@slow/,373shard: { current: 1, total: 4 },374},375{376name: "shard-2",377use: { ...devices["Desktop Chrome"] },378shard: { current: 2, total: 4 },379},380// ... more shards381],382});383384// Run in CI385// npx playwright test --shard=1/4386// npx playwright test --shard=2/4387```388389### Pattern 3: Accessibility Testing390391```typescript392// Install: npm install @axe-core/playwright393import { test, expect } from "@playwright/test";394import AxeBuilder from "@axe-core/playwright";395396test("page should not have accessibility violations", async ({ page }) => {397await page.goto("/");398399const accessibilityScanResults = await new AxeBuilder({ page })400.exclude("#third-party-widget")401.analyze();402403expect(accessibilityScanResults.violations).toEqual([]);404});405406test("form is accessible", async ({ page }) => {407await page.goto("/signup");408409const results = await new AxeBuilder({ page }).include("form").analyze();410411expect(results.violations).toEqual([]);412});413```414