e2e-testing-patternsBuild reliable, fast E2E test suites with Playwright and Cypress. Critical user journey coverage, flaky test elimination, CI/CD integration.
Install via ClawdBot CLI:
clawdbot install wpank/e2e-testing-patternsTest what users do, not how code works. E2E tests prove the system works as a whole — they're your confidence to ship.
npx clawhub@latest install e2e-testing-patterns
Provides patterns for building end-to-end test suites that:
/\
/E2E\ ← FEW: Critical paths only (this skill)
/─────\
/Integr\ ← MORE: Component interactions, API contracts
/────────\
/Unit Tests\ ← MANY: Fast, isolated, cover edge cases
/────────────\
| E2E Tests ✓ | NOT E2E Tests ✗ |
|-------------|-----------------|
| Critical user journeys (login → dashboard → action → logout) | Unit-level logic (use unit tests) |
| Multi-step flows (checkout, onboarding wizard) | API contracts (use integration tests) |
| Cross-browser compatibility | Edge cases (too slow, use unit tests) |
| Real API integration | Internal implementation details |
| Authentication flows | Component visual states (use Storybook) |
Rule of thumb: If it would devastate your business to break, E2E test it. If it's just inconvenient, test it faster with unit/integration tests.
| Principle | Why | How |
|-----------|-----|-----|
| Test behavior, not implementation | Survives refactors | Assert on user-visible outcomes, not DOM structure |
| Independent tests | Parallelizable, debuggable | Each test creates its own data, cleans up after |
| Deterministic waits | No flakiness | Wait for conditions, not fixed timeouts |
| Stable selectors | Survives UI changes | Use data-testid, roles, labels — never CSS classes |
| Fast feedback | Developers run them | Mock external services, parallelize, shard |
// 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"] } },
],
});
Encapsulate page logic. Tests read like user stories.
// 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();
}
}
// tests/login.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";
test("successful login redirects to dashboard", 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();
});
Create and clean up test data automatically.
// fixtures/test-data.ts
import { test as base } from "@playwright/test";
export const test = base.extend<{ testUser: TestUser }>({
testUser: async ({}, use) => {
// Setup: Create user
const user = await createTestUser({
email: `test-${Date.now()}@example.com`,
password: "Test123!@#",
});
await use(user);
// Teardown: Clean up
await deleteTestUser(user.id);
},
});
// Usage — testUser is created before, deleted after
test("user can update profile", async ({ page, testUser }) => {
await page.goto("/login");
await page.getByLabel("Email").fill(testUser.email);
// ...
});
Never use fixed timeouts. Wait for specific conditions.
// ❌ FLAKY: Fixed timeout
await page.waitForTimeout(3000);
// ✅ STABLE: Wait for conditions
await page.waitForLoadState("networkidle");
await page.waitForURL("/dashboard");
// ✅ BEST: Auto-waiting assertions
await expect(page.getByText("Welcome")).toBeVisible();
await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled();
// Wait for API response
const responsePromise = page.waitForResponse(
(r) => r.url().includes("/api/users") && r.status() === 200
);
await page.getByRole("button", { name: "Load" }).click();
await responsePromise;
Isolate tests from real external services.
test("shows error when API fails", async ({ page }) => {
// Mock the API response
await page.route("**/api/users", (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: "Server Error" }),
});
});
await page.goto("/users");
await expect(page.getByText("Failed to load users")).toBeVisible();
});
test("handles slow network gracefully", async ({ page }) => {
await page.route("**/api/data", async (route) => {
await new Promise((r) => setTimeout(r, 3000)); // Simulate delay
await route.continue();
});
await page.goto("/dashboard");
await expect(page.getByText("Loading...")).toBeVisible();
});
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
dataCy(value: string): Chainable<JQuery<HTMLElement>>;
}
}
}
Cypress.Commands.add("login", (email, password) => {
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("dataCy", (value) => {
return cy.get(`[data-cy="${value}"]`);
});
// Usage
cy.login("user@example.com", "password");
cy.dataCy("submit-button").click();
// Mock API
cy.intercept("GET", "/api/users", {
statusCode: 200,
body: [{ id: 1, name: "John" }],
}).as("getUsers");
cy.visit("/users");
cy.wait("@getUsers");
cy.get('[data-testid="user-list"]').children().should("have.length", 1);
| Priority | Selector Type | Example | Why |
|----------|--------------|---------|-----|
| 1 | Role + name | getByRole("button", { name: "Submit" }) | Accessible, user-facing |
| 2 | Label | getByLabel("Email address") | Accessible, semantic |
| 3 | data-testid | getByTestId("checkout-form") | Stable, explicit for testing |
| 4 | Text content | getByText("Welcome back") | User-facing |
| ❌ | CSS classes | .btn-primary | Breaks on styling changes |
| ❌ | DOM structure | div > form > input:nth-child(2) | Breaks on any restructure |
// ❌ BAD: Brittle selectors
cy.get(".btn.btn-primary.submit-button").click();
cy.get("div > form > div:nth-child(2) > input").type("text");
// ✅ GOOD: Stable selectors
page.getByRole("button", { name: "Submit" }).click();
page.getByLabel("Email address").fill("user@example.com");
page.getByTestId("email-input").fill("user@example.com");
// Playwright visual comparisons
test("homepage looks correct", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveScreenshot("homepage.png", {
fullPage: true,
maxDiffPixels: 100,
});
});
test("button states", async ({ page }) => {
const button = page.getByRole("button", { name: "Submit" });
await expect(button).toHaveScreenshot("button-default.png");
await button.hover();
await expect(button).toHaveScreenshot("button-hover.png");
});
// npm install @axe-core/playwright
import AxeBuilder from "@axe-core/playwright";
test("page has no accessibility violations", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page })
.exclude("#third-party-widget") // Exclude things you can't control
.analyze();
expect(results.violations).toEqual([]);
});
# Run in headed mode (see the browser)
npx playwright test --headed
# Debug mode (step through)
npx playwright test --debug
# Show trace viewer for failed tests
npx playwright show-report
// Add test steps for better failure reports
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("Complete checkout", async () => {
await page.goto("/checkout");
// ... if this fails, you know which step
});
});
// Pause for manual inspection
await page.pause();
When a test fails intermittently, check:
| Issue | Fix |
|-------|-----|
| Fixed waitForTimeout() calls | Replace with waitForSelector() or expect assertions |
| Race conditions on page load | Wait for networkidle or specific elements |
| Test data pollution | Ensure tests create/clean their own data |
| Animation timing | Wait for animations to complete or disable them |
| Viewport inconsistency | Set explicit viewport in config |
| Random test order issues | Tests must be independent |
| Third-party service flakiness | Mock external APIs |
# GitHub Actions example
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: npm run start & npx wait-on http://localhost:3000
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
waitForTimeout() or cy.wait(ms) — they cause flaky tests and slow down suites// Navigation
await page.goto("/path");
await page.goBack();
await page.reload();
// Interactions
await page.click("selector");
await page.fill("selector", "text");
await page.type("selector", "text"); // Types character by character
await page.selectOption("select", "value");
await page.check("checkbox");
// Assertions
await expect(page).toHaveURL("/expected");
await expect(locator).toBeVisible();
await expect(locator).toHaveText("expected");
await expect(locator).toBeEnabled();
await expect(locator).toHaveCount(3);
// Navigation
cy.visit("/path");
cy.go("back");
cy.reload();
// Interactions
cy.get("selector").click();
cy.get("selector").type("text");
cy.get("selector").clear().type("text");
cy.get("select").select("value");
cy.get("checkbox").check();
// Assertions
cy.url().should("include", "/expected");
cy.get("selector").should("be.visible");
cy.get("selector").should("have.text", "expected");
cy.get("selector").should("have.length", 3);
Generated Mar 1, 2026
An online retailer needs to ensure the checkout process works flawlessly across browsers and devices. This scenario involves testing adding items to cart, applying promo codes, entering shipping details, and completing payment. Using Playwright's multi-browser projects and page object model, tests simulate real user journeys to catch regressions before deployment.
A software-as-a-service company wants to validate its multi-step onboarding flow for new users. This includes account creation, feature tours, and initial setup steps. Implementing fixtures for test data ensures each test runs independently with clean user data, while smart waiting patterns eliminate flakiness from network delays.
A financial institution requires reliable E2E tests for critical paths like login, balance checks, and fund transfers. Tests must be deterministic and fast for CI/CD pipelines, using stable selectors like data-testid to survive UI updates. Playwright's trace and screenshot features aid in debugging failures in production-like environments.
A healthcare provider needs to test patient registration flows that involve form submissions, validation, and integration with backend APIs. By applying the test pyramid principle, E2E tests focus on end-user journeys while mocking external services for speed. This ensures compliance and data accuracy without over-testing edge cases.
A media company automates testing for its content management system, covering article creation, editing, and publishing. Using the page object model, tests encapsulate page logic to resemble user stories, making them maintainable. CI/CD integration with retries and parallel workers ensures fast feedback during continuous deployment cycles.
Companies offering monthly or annual subscriptions rely on E2E tests to ensure core features like user onboarding and billing workflows are reliable. Fast, stable tests in CI/CD pipelines reduce churn by preventing regressions that could disrupt service. This model benefits from testing critical user journeys to maintain customer trust and retention.
Platforms connecting buyers and sellers need robust E2E tests for transaction flows, search functionality, and user reviews. By eliminating flaky tests with deterministic waits and independent test data, they can deploy frequently without breaking key features. This supports high-volume sales and minimizes downtime-related revenue loss.
Businesses selling software to other enterprises use E2E tests to validate complex, multi-step workflows like data integration and reporting. Testing cross-browser compatibility and real API integrations ensures software works in diverse client environments. This reduces support costs and enhances contract renewals through reliable performance.
💬 Integration Tip
Integrate this skill into CI/CD pipelines by configuring Playwright with retries for flaky tests and using parallel execution to speed up feedback loops. Mock external dependencies to keep tests fast and reliable.
Use the mcporter CLI to list, configure, auth, and call MCP servers/tools directly (HTTP or stdio), including ad-hoc servers, config edits, and CLI/type generation.
Connect to 100+ APIs (Google Workspace, Microsoft 365, GitHub, Notion, Slack, Airtable, HubSpot, etc.) with managed OAuth. Use this skill when users want to...
Build, debug, and deploy websites using HTML, CSS, JavaScript, and modern frameworks following production best practices.
YouTube Data API integration with managed OAuth. Search videos, manage playlists, access channel data, and interact with comments. Use this skill when users want to interact with YouTube. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway).
Scaffold, test, document, and debug REST and GraphQL APIs. Use when the user needs to create API endpoints, write integration tests, generate OpenAPI specs, test with curl, mock APIs, or troubleshoot HTTP issues.
Search for jobs across LinkedIn, Indeed, Glassdoor, ZipRecruiter, Google Jobs, Bayt, Naukri, and BDJobs using the JobSpy MCP server.