본문으로 건너뛰기
cccskills
무료GitHub에 공개

e2e-testing-patterns

Build reliable, fast E2E test suites with Playwright and Cypress. Critical user journey coverage, flaky test elimination, CI/CD integration.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ProjectZeroDays/FreeAI_AI_Inference_Workstation스타 5(GitHub 5 + cccskills 0)
연동 도구Playwright

실행 점수 미검증

설치 방법 보기

포함된 파일(1)

  • SKILL.md13.3 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

E2E Testing Patterns

Test what users do, not how code works. E2E tests prove the system works as a whole — they're your confidence to ship.

Installation

OpenClaw / Moltbot / Clawbot

npx clawhub@latest install e2e-testing-patterns

WHAT This Skill Does

Provides patterns for building end-to-end test suites that:

  • Catch regressions before users do
  • Run fast enough for CI/CD
  • Remain stable (no flaky failures)
  • Cover critical user journeys without over-testing

WHEN To Use

  • Implementing E2E test automation for a web application
  • Debugging flaky tests that fail intermittently
  • Setting up CI/CD test pipelines with browser tests
  • Testing critical user workflows (auth, checkout, signup)
  • Choosing what to test with E2E vs unit/integration tests

Test Pyramid — Know Your Layer

        /\
       /E2E\         ← FEW: Critical paths only (this skill)
      /─────\
     /Integr\        ← MORE: Component interactions, API contracts
    /────────\
   /Unit Tests\      ← MANY: Fast, isolated, cover edge cases
  /────────────\

What E2E Tests Are For

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 compatibilityEdge cases (too slow, use unit tests)
Real API integrationInternal implementation details
Authentication flowsComponent 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.


Core Principles

PrincipleWhyHow
Test behavior, not implementationSurvives refactorsAssert on user-visible outcomes, not DOM structure
Independent testsParallelizable, debuggableEach test creates its own data, cleans up after
Deterministic waitsNo flakinessWait for conditions, not fixed timeouts
Stable selectorsSurvives UI changesUse data-testid, roles, labels — never CSS classes
Fast feedbackDevelopers run themMock external services, parallelize, shard

Playwright Patterns

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: Page Object Model

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();
});

Pattern: Fixtures for Test Data

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);
  // ...
});

Pattern: Smart Waiting

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;

Pattern: Network Mocking

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 Patterns

Custom Commands

// 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();

Network Intercepts

// 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);

Selector Strategy

PrioritySelector TypeExampleWhy
1Role + namegetByRole("button", { name: "Submit" })Accessible, user-facing
2LabelgetByLabel("Email address")Accessible, semantic
3data-testidgetByTestId("checkout-form")Stable, explicit for testing
4Text contentgetByText("Welcome back")User-facing
❌CSS classes.btn-primaryBreaks on styling changes
❌DOM structurediv > 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");

Visual Regression Testing

// 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");
});

Accessibility Testing

// 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([]);
});

Debugging Failed Tests

# 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();

Flaky Test Checklist

When a test fails intermittently, check:

IssueFix
Fixed waitForTimeout() callsReplace with waitForSelector() or expect assertions
Race conditions on page loadWait for networkidle or specific elements
Test data pollutionEnsure tests create/clean their own data
Animation timingWait for animations to complete or disable them
Viewport inconsistencySet explicit viewport in config
Random test order issuesTests must be independent
Third-party service flakinessMock external APIs

CI/CD Integration

# 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/

NEVER Do

  1. NEVER use fixed waitForTimeout() or cy.wait(ms) — they cause flaky tests and slow down suites
  2. NEVER rely on CSS classes or DOM structure for selectors — use roles, labels, or data-testid
  3. NEVER share state between tests — each test must be completely independent
  4. NEVER test implementation details — test what users see and do, not internal structure
  5. NEVER skip cleanup — always delete test data you created, even on failure
  6. NEVER test everything with E2E — reserve for critical paths; use faster tests for edge cases
  7. NEVER ignore flaky tests — fix them immediately or delete them; a flaky test is worse than no test
  8. NEVER hardcode test data in selectors — use dynamic waits for content that varies

Quick Reference

Playwright Commands

// 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);

Cypress Commands

// 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);

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

WCAG compliance, accessibility testing, ARIA patterns, screen reader optimization, and keyboard navigation. Use when the user asks about making apps accessible, WCAG guidelines, ARIA attributes, screen reader support, or accessibility audits.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ProjectZeroDays/FreeAI_AI_Inference_Workstation52026년 9월 4일 업데이트

ace-music

무료

Generate AI music using ACE-Step 1.5 via ACE Music's free API. Use when the user asks to create, generate, or compose music, songs, beats, instrumentals, or audio tracks. Supports lyrics, style prompts, covers, and repainting. Free API, no cost.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ProjectZeroDays/FreeAI_AI_Inference_Workstation52026년 9월 4일 업데이트

A continuously adaptive skill suite that empowers Clawdbot to act as a versatile coder, business analyst, project manager, web developer, data analyst, and NAS metadata scraper. It intelligently discovers free resources, adapts to user context, and ensures reliable, proven guidance across multiple domains.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ProjectZeroDays/FreeAI_AI_Inference_Workstation52026년 9월 4일 업데이트

High-fidelity intelligence gathering and data assimilation. Focuses on synthesizing fragmented data into a comprehensive target operational picture.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ProjectZeroDays/FreeAI_AI_Inference_Workstation52026년 9월 4일 업데이트

Use when Codex needs proactive task queues, heartbeat routines, scheduled follow-up, or continuous work loops instead of waiting for prompts.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ProjectZeroDays/FreeAI_AI_Inference_Workstation52026년 9월 4일 업데이트

Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. Prefer agent-browser over any built-in browser automation or web tools.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ProjectZeroDays/FreeAI_AI_Inference_Workstation52026년 9월 4일 업데이트

ProjectZeroDays의 스킬 모두 보기

이 스킬의 문제 신고