mit einem Klick
e2e-tester
// Playwright E2E testing patterns. Trigger: When writing Playwright E2E tests (Page Object Model, selectors, MCP exploration workflow).
// Playwright E2E testing patterns. Trigger: When writing Playwright E2E tests (Page Object Model, selectors, MCP exploration workflow).
Guides creation of comprehensive Product Requirement Documents (PRDs) for software projects through structured questioning and validation, then generates implementation task lists in JSON format. Use when users want to document a software idea, create specifications for development, plan a new application feature/bug, or break down requirements into actionable tasks. Transforms ideas into implementation-ready documents with verifiable pass criteria.
Refactor high-complexity React components in frontend. Use when the user asks for code splitting, hook extraction, or complexity reduction, or when you come across a component that is too complex to understand and refactor it.
Trigger when the user requests a review of frontend files (e.g., `.tsx`, `.ts`, `.js`). Support both pending-change reviews and focused file reviews while applying the checklist rules.
Generate Vitest + React Testing Library tests for frontend components, hooks, and utilities. Triggers on testing, spec files, coverage, Vitest, RTL, unit tests, integration tests, or write/review test requests.
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
Comprehensive vitest testing patterns covering test structure, AAA pattern, parameterized tests, assertions, mocking, test doubles, error handling, async testing, and performance optimization. Use when writing, reviewing, or refactoring vitest tests, or when user mentions vitest, testing, TDD, test coverage, mocking, assertions, or test files (*.test.ts, *.spec.ts).
| name | e2e-tester |
| description | Playwright E2E testing patterns. Trigger: When writing Playwright E2E tests (Page Object Model, selectors, MCP exploration workflow). |
| metadata | {"scope":["root","ui"],"auto_invoke":"Writing Playwright E2E tests"} |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task |
Before you start:
If the answer is yes to any of the above, proceed with the end to end test creation.
⚠️ If you have Playwright MCP tools, ALWAYS use them BEFORE creating any test:
If MCP NOT available: Proceed with test creation based on docs and code analysis.
Why This Matters:
// ❌ NEVER use fixed waits
await page.waitForTimeout(2000);
// ✅ Wait for specific conditions
await expect(element).toBeVisible();
await page.waitForResponse(resp => resp.url().includes('/api/data'));
await page.waitForURL('**/dashboard');
await expect(page.getByText('Success')).toBeVisible({ timeout: 10000 });
// ✅ For SPAs, prefer explicit waits over networkidle
await page.goto('/app', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('main')).toBeVisible();
tests/
├── base-page.ts # Parent class for ALL pages
├── helpers.ts # Shared utilities
└── {page-name}/
├── {page-name}-page.ts # Page Object Model
├── {page-name}.spec.ts # ALL tests here (NO separate files!)
└── {page-name}.md # Test documentation
File Naming:
sign-up.spec.ts (all sign-up tests)sign-up-page.ts (page object)sign-up.md (documentation)sign-up-critical-path.spec.ts (WRONG - no separate files)sign-up-validation.spec.ts (WRONG)// 1. BEST - getByRole for interactive elements
this.submitButton = page.getByRole("button", { name: "Submit" });
this.navLink = page.getByRole("link", { name: "Dashboard" });
// 2. BEST - getByLabel for form controls
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
// 3. SPARINGLY - getByText for static content only
this.errorMessage = page.getByText("Invalid credentials");
this.pageTitle = page.getByText("Welcome");
// 4. LAST RESORT - getByTestId when above fail
this.customWidget = page.getByTestId("date-picker");
// ❌ AVOID fragile selectors
this.button = page.locator(".btn-primary"); // NO
this.input = page.locator("#email"); // NO
| User Says | Action |
|---|---|
| "a test", "one test", "new test", "add test" | Create ONE test() in existing spec |
| "comprehensive tests", "all tests", "test suite", "generate tests" | Create full suite |
Examples:
import { Page, Locator, expect } from "@playwright/test";
// BasePage - ALL pages extend this
export class BasePage {
constructor(protected page: Page) {}
async goto(path: string): Promise<void> {
await this.page.goto(path);
await this.page.waitForLoadState("networkidle");
}
// Common methods go here (see Refactoring Guidelines)
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
async verifyNotificationMessage(message: string): Promise<void> {
const notification = this.page.locator('[role="status"]');
await expect(notification).toContainText(message);
}
}
// Page-specific implementation
export interface LoginData {
email: string;
password: string;
}
export class LoginPage extends BasePage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.submitButton = page.getByRole("button", { name: "Sign in" });
}
async goto(): Promise<void> {
await super.goto("/login");
}
async login(data: LoginData): Promise<void> {
await this.emailInput.fill(data.email);
await this.passwordInput.fill(data.password);
await this.submitButton.click();
}
async verifyCriticalOutcome(): Promise<void> {
await expect(this.page).toHaveURL("/dashboard");
}
}
Always check existing page objects before creating new ones!
// ✅ GOOD: Reuse existing page objects
import { SignInPage } from "../sign-in/sign-in-page";
import { HomePage } from "../home/home-page";
test("User can sign up and login", async ({ page }) => {
const signUpPage = new SignUpPage(page);
const signInPage = new SignInPage(page); // REUSE
const homePage = new HomePage(page); // REUSE
await signUpPage.signUp(userData);
await homePage.verifyPageLoaded(); // REUSE method
await homePage.signOut(); // REUSE method
await signInPage.login(credentials); // REUSE method
});
// ❌ BAD: Recreating existing functionality
export class SignUpPage extends BasePage {
async logout() { /* ... */ } // ❌ HomePage already has this
async login() { /* ... */ } // ❌ SignInPage already has this
}
Guidelines:
tests/ for existing page objects firstBasePage when:waitForPageLoad(), getCurrentUrl())isVisible(), waitForVisible())helpers.ts when:generateUniqueEmail(), generateTestUser())createTestUser(), cleanupTestData())expectNotificationToContain())seedDatabase(), resetState())waitForCondition(), retryAction())Before (BAD):
// Repeated in multiple page objects
export class SignUpPage extends BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
}
export class SignInPage extends BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]'); // DUPLICATED!
}
}
After (GOOD):
// BasePage - shared across all pages
export class BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
}
// helpers.ts - data generation
export function generateUniqueEmail(): string {
return `test.${Date.now()}@example.com`;
}
export function generateTestUser() {
return {
name: "Test User",
email: generateUniqueEmail(),
password: "TestPassword123!",
};
}
import { test, expect } from "@playwright/test";
import { LoginPage } from "./login-page";
test.describe("Login", () => {
test("User can login successfully",
{ tag: ["@critical", "@e2e", "@login", "@LOGIN-E2E-001"] },
async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login({ email: "user@test.com", password: "pass123" });
await expect(page).toHaveURL("/dashboard");
}
);
});
Tag Categories:
@critical, @high, @medium, @low@e2e@signup, @signin, @dashboard@SIGNUP-E2E-001, @LOGIN-E2E-002### E2E Tests: {Feature Name}
**Suite ID:** `{SUITE-ID}`
**Feature:** {Feature description}
---
## Test Case: `{TEST-ID}` - {Test case title}
**Priority:** `{critical|high|medium|low}`
**Tags:**
- type → @e2e
- feature → @{feature-name}
**Description/Objective:** {Brief description}
**Preconditions:**
- {Prerequisites for test to run}
- {Required data or state}
### Flow Steps:
1. {Step 1}
2. {Step 2}
3. {Step 3}
### Expected Result:
- {Expected outcome 1}
- {Expected outcome 2}
### Key verification points:
- {Assertion 1}
- {Assertion 2}
### Notes:
- {Additional considerations}
Documentation Rules:
// auth.setup.ts - Run once, reuse across tests
import { test as setup } from "@playwright/test";
setup("authenticate", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("user@test.com");
await page.getByLabel("Password").fill("password");
await page.getByRole("button", { name: "Sign in" }).click();
await page.waitForURL("/dashboard");
await page.context().storageState({ path: ".auth/user.json" });
});
// playwright.config.ts
projects: [
{ name: "auth", testMatch: /auth\.setup\.ts/ },
{ name: "logged-in", dependencies: ["auth"], use: { storageState: ".auth/user.json" } },
{ name: "logged-out", use: { storageState: { cookies: [], origins: [] } } }
]
// Mock API responses for isolated tests
await page.route("**/api/users", route =>
route.fulfill({ json: { users: [{ id: 1, name: "Test" }] } })
);
// Mock error states
await page.route("**/api/submit", route =>
route.fulfill({ status: 500, json: { error: "Server error" } })
);
// Abort requests (e.g., block analytics)
await page.route("**/analytics/**", route => route.abort());
// fixtures.ts - Custom fixtures for setup/teardown
import { test as base } from "@playwright/test";
import { AdminPage } from "./admin/admin-page";
export const test = base.extend<{ adminPage: AdminPage }>({
adminPage: async ({ page }, use) => {
const admin = new AdminPage(page);
await admin.goto();
await use(admin);
// Teardown runs after test
},
});
// Usage in tests
test("admin can manage users", async ({ adminPage }) => {
await adminPage.deleteUser("test@example.com");
});
// Tests run in parallel by default. Use serial when tests share state:
test.describe.configure({ mode: "serial" });
// Isolate test data to prevent conflicts
test("create user", async ({ page }) => {
const uniqueEmail = `user-${Date.now()}@test.com`; // ✅ Unique per run
});
// Soft assertions - collect multiple failures
await expect.soft(page.getByText("Title")).toBeVisible();
await expect.soft(page.getByText("Subtitle")).toBeVisible();
// Test continues, reports all failures at end
// Polling assertions - retry until condition met
await expect(async () => {
const count = await page.getByRole("listitem").count();
expect(count).toBeGreaterThan(5);
}).toPass({ timeout: 10000 });
// Visual regression
await expect(page).toHaveScreenshot("dashboard.png");
await expect(page.getByRole("dialog")).toHaveScreenshot();
// ❌ Element detached after navigation
const button = page.getByRole("button", { name: "Submit" });
await button.click();
await button.click(); // May fail if page navigated
// ✅ Re-query after navigation
await page.getByRole("button", { name: "Submit" }).click();
await page.getByRole("button", { name: "Confirm" }).click();
// ❌ Race condition with animations
await element.click();
// ✅ Wait for animation to complete
await element.click();
await expect(modal).toBeVisible();
// Iframes
const frame = page.frameLocator("#iframe-id");
await frame.getByRole("button").click();
// Shadow DOM - Playwright pierces by default, but for closed shadow:
await page.locator("custom-element").locator("internal:shadow=button").click();
// In test file
test.use({ viewport: { width: 375, height: 667 } });
// Device emulation
import { devices } from "@playwright/test";
test.use({ ...devices["iPhone 13"] });
// Or in playwright.config.ts projects
projects: [
{ name: "desktop", use: { viewport: { width: 1280, height: 720 } } },
{ name: "mobile", use: { ...devices["iPhone 13"] } },
]
# Enable traces on failure (recommended for CI)
npx playwright test --trace on-first-retry
# View trace file
npx playwright show-trace trace.zip
# Debug mode - step through test
npx playwright test --debug
# Headed mode to see browser
npx playwright test --headed
// In playwright.config.ts
use: {
trace: "on-first-retry", // Capture trace on retry
screenshot: "only-on-failure", // Screenshot on failure
video: "retain-on-failure", // Video on failure
}
// playwright.config.ts for CI
export default defineConfig({
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI
? [["html"], ["github"], ["junit", { outputFile: "results.xml" }]]
: [["html"]],
use: {
trace: "on-first-retry",
screenshot: "only-on-failure",
},
});
# GitHub Actions example
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
Always prefer running specific tests over the entire suite for faster feedback:
# Run ALL tests
npx playwright test
# ✅ PREFERRED: Run specific file
npx playwright test tests/login/login.spec.ts
# ✅ PREFERRED: Run specific folder
npx playwright test tests/login/
# ✅ PREFERRED: Run by test name (grep)
npx playwright test --grep "login"
npx playwright test --grep "user can sign up"
# ✅ PREFERRED: Run by tag
npx playwright test --grep "@critical"
npx playwright test --grep "@LOGIN-E2E-001"
# ✅ Run single test by line number
npx playwright test tests/login/login.spec.ts:42
# ✅ Run tests matching multiple patterns
npx playwright test --grep "login|signup"
# ✅ Exclude tests by pattern
npx playwright test --grep-invert "slow"
npx playwright test --debug # Debug mode (step through)
npx playwright test --headed # See browser
npx playwright test --trace on # Enable tracing
npx playwright test --project=mobile # Run specific project
npx playwright codegen # Record tests
Always use partial/pattern matching instead of exact strings to reduce maintenance:
// ❌ FRAGILE: Exact string matches break easily
await expect(page.getByText("Welcome to Our Application!")).toBeVisible();
await expect(page.getByRole("button", { name: "Submit Form" })).toBeVisible();
await expect(notification).toHaveText("Your account has been created successfully.");
// ✅ ROBUST: Partial matches survive copy changes
await expect(page.getByText(/welcome/i)).toBeVisible();
await expect(page.getByRole("button", { name: /submit/i })).toBeVisible();
await expect(notification).toContainText("account");
await expect(notification).toContainText(/created/i);
// ✅ ROBUST: Use toContainText over toHaveText
await expect(page.locator(".message")).toContainText("success");
// ✅ ROBUST: Pattern matching for dynamic content
await expect(page.getByText(/order #\d+/i)).toBeVisible();
await expect(page.getByText(/\d+ items? in cart/i)).toBeVisible();
Why Partial Matching: