skills/testing-e2e/SKILL.md
Expert guidance for writing resilient end-to-end tests that span multiple processes and components. Use this skill when reviewing, writing, or refactoring system-wide e2e tests. Covers user-facing scenarios, helper functions, data factories, ARIA-based selectors, and auto-retriable assertions. NOT for unit/integration tests - use testing-unit-integration skill instead. See references/playwright-resilience.md for detailed selector patterns.
npx skillsauth add agdev/claude-code testing-e2eInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Expert guidance for system-wide e2e tests spanning multiple processes.
Scope: System-wide end-to-end tests only. NOT for unit, integration, or component tests with mocked backends - use testing-unit-integration skill instead.
Note: E2E tests are complex, slow, and limited in quantity - relaxed readability standards and less stringent best practices are acceptable compared to unit tests.
getByRole, getByLabel, getByTextEach helper represents a meaningful part of user's journey, not low-level interactions:
// ✅ GOOD - Semantic helpers
async function loginAsAdmin(page: Page): Promise<void>
async function addProductToCart(page: Page, product: Product): Promise<void>
async function completeCheckout(page: Page): Promise<OrderDetails>
async function verifyOrderConfirmationEmailSent(email: string, order: OrderDetails): Promise<void>
// ❌ BAD - Low-level interactions exposed
async function clickButton(page: Page, name: string): Promise<void>
async function fillInput(page: Page, selector: string, value: string): Promise<void>
setTimeout, page.waitForTimeout()waitForSelector - couples test to implementation// ❌ BAD
await page.waitForTimeout(3000)
await page.waitForSelector('.form-loaded')
// ✅ GOOD - Auto-retriable assertions
await expect(page.getByText(/success/i)).toBeVisible()
await expect(page).toHaveURL(/\/dashboard/)
buildEntity() functionsimport { faker } from "@faker-js/faker";
import { Customer } from "../types";
export function buildCustomer(overrides: Partial<Customer> = {}): Customer {
return {
email: faker.internet.email(),
name: faker.person.fullName(),
address: faker.location.streetAddress(),
...overrides,
};
}
// ❌ WEAK - Multiple redundant assertions
expect(response).not.toBeNull()
expect(Array.isArray(response)).toBe(true)
expect(response.length).toBe(2)
// ✅ STRONG - Single assertion catches all
expect(response).toEqual([{id: '123'}, {id: '456'}])
E2E tests catch navigation bugs that mocked unit/integration tests miss.
// ❌ BAD - Testing that navigate function was called (implementation detail)
expect(mockNavigate).toHaveBeenCalledWith('/users/123');
// ❌ BAD - Hardcoded path string (typos won't be caught)
await page.goto('/usres/123'); // Typo! Would fail silently in unit test
// ✅ GOOD - Assert the OUTCOME (user sees the page)
await expect(page.getByRole('heading', { name: /user profile/i })).toBeVisible();
// ✅ GOOD - Verify URL changed
await expect(page).toHaveURL(/\/users\/\d+/);
// ✅ GOOD - Use route constants (from shared constants file)
import { ROUTES } from '@/constants/routes';
await page.goto(ROUTES.USER_PROFILE.replace(':id', '123'));
| Bug Type | Unit Test (Mocked) | E2E Test | |----------|-------------------|----------| | Route typo in navigate('/usres') | ❌ Passes (mock doesn't validate) | ✅ Fails (404 page) | | Route param mismatch (email vs id) | ❌ Passes (mock accepts anything) | ✅ Fails (wrong page loads) | | Missing route in router config | ❌ Passes (mock doesn't use router) | ✅ Fails (404 or error) | | Backend contract change | ❌ Passes (mock returns fantasy data) | ✅ Fails (real API different) |
E2E tests are expensive - maximize value per test:
test('Should purchase item', async ({ page }) => { // ❌ A.5 - vague title
await page.goto('/products') // ❌ A.8, A.11 - assumes existing session
await page.getByText('iPhone 15 Pro').click() // ❌ A.8 - assumes specific product
await page.locator('#add-to-cart-btn').click() // ❌ A.17 - CSS selector
await page.goto('/checkout')
await page.waitForSelector('.form-loaded') // ❌ A.44 - implementation detail
await page.locator('#email').fill('[email protected]') // ❌ A.17, A.55 - CSS + dummy data
await page.locator('button').last().click() // ❌ A.17, A.40 - positional
const cartItems = await page.locator('.cart-item').all() // ❌ A.58 - custom loop
for (const item of cartItems) expect(await item.isVisible()).toBe(true) // ❌ A.58
await page.goto('https://stripe.com/confirm') // ❌ A.23 - external system
expect(await page.evaluate(() => localStorage.getItem('orderId'))).toBeTruthy() // ❌ A.14 - implementation
})
test('The user can purchase an item and post-purchase experience is valid', async ({ page }) => {
// Arrange - Create fresh test data
const customer = await saveNewCustomer(buildCustomer())
const product = await saveNewProduct(buildProduct())
// Act - User journey through semantic helpers
await navigateToProductsPage(page)
await selectProduct(page, product)
await goToCheckout(page)
await fillCustomerDetails(page, customer)
await fillPaymentDetails(page, buildCreditCard())
// Assert - Verify outcomes
const orderDetails = await submitOrderAndVerifySuccessPage(page)
await verifyOrderConfirmationEmailSent(customer.email, orderDetails)
await verifySupplyRequestWasCreated(customer.email, orderDetails)
})
For detailed selector patterns, page object best practices, and resilience strategies, see:
references/playwright-resilience.md - Comprehensive guide including:
When reviewing e2e tests, report violations as:
Line X: Violates [RULE_NUMBER] - [Brief explanation]
Example:
Line 8: Violates A.17 - Uses CSS selector '#email', should use getByLabel('Email')
Line 12: Violates A.44 - Uses waitForTimeout, should use auto-retriable assertion
Line 15: Violates A.23 - Navigates to external domain stripe.com
development
Expert guidance for writing clean, simple, and effective unit, integration, component, microservice, and API tests. Use this skill when reviewing existing tests for violations, writing new tests, or refactoring tests. NOT for end-to-end tests that span multiple processes - use testing-e2e skill instead. Covers AAA pattern, data factories, mocking strategies, DOM testing, database testing, and assertion best practices.
tools
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.
data-ai
Expert guidance for creating, updating, and maintaining CLAUDE.md memory files following best practices for hierarchical memory, structure, content organization, and team collaboration
tools
Expert guidance for creating effective Claude Code agents (subagents). Use when users want to create a new agent, update an existing agent, or learn agent design best practices. Covers agent architecture, prompt engineering, tool selection, model choice, and common pitfalls. Integrates with skill-creator when agent needs accompanying skills.