From tdd-dev-workflow
This skill should be used when the user asks about "e2e testing", "end-to-end tests", "when to write e2e tests", "e2e test strategy", "what should I e2e test", "e2e test structure", "browser testing strategy", "data-testid selectors", "flaky e2e tests", "e2e vs unit tests", "Cypress", or needs guidance on when, what, and how to structure end-to-end tests. For Playwright-specific API patterns and configuration, see playwright-patterns instead. Provides E2E testing strategy and principles.
How this skill is triggered — by the user, by Claude, or both
Slash command
/tdd-dev-workflow:e2e-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Validate complete user workflows through the actual UI. E2E tests complement unit and integration tests by testing real user journeys end-to-end, verifying that all layers of the application work together correctly from the user's perspective.
Validate complete user workflows through the actual UI. E2E tests complement unit and integration tests by testing real user journeys end-to-end, verifying that all layers of the application work together correctly from the user's perspective.
E2E tests are the final validation layer, not a replacement for unit tests.
Encapsulate page-specific selectors and actions in Page Object classes. Tests interact with pages through these objects, not directly with selectors.
// pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.fill('[data-testid="email"]', email);
await this.page.fill('[data-testid="password"]', password);
await this.page.click('[data-testid="submit"]');
}
async getErrorMessage() {
return this.page.textContent('[data-testid="error-message"]');
}
}
// tests/login.e2e.ts
test('displays error for invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('[email protected]', 'wrongpassword');
const error = await loginPage.getErrorMessage();
expect(error).toContain('Invalid credentials');
});
Name tests to match user stories and acceptance criteria:
// Good: describes the user journey
'user can log in and see the dashboard'
'submitting empty form shows validation errors'
'admin can approve pending requests'
// Bad: describes mechanics
'click login button'
'check form validation'
'test admin page'
Isolate test data to prevent tests from interfering with each other:
Before writing code, outline the journey in plain language:
1. User navigates to the registration page.
2. User fills in name, email, and password.
3. User submits the form.
4. System displays a confirmation message.
5. User receives a verification email.
Translate each journey step into page actions:
test('new user can register successfully', async ({ page }) => {
const registrationPage = new RegistrationPage(page);
await registrationPage.navigate();
await registrationPage.fillForm({
name: 'Test User',
email: '[email protected]',
password: 'SecurePass123!',
});
await registrationPage.submit();
await expect(page.locator('[data-testid="confirmation"]'))
.toBeVisible();
await expect(page.locator('[data-testid="confirmation"]'))
.toContainText('Registration successful');
});
Web applications involve network requests, animations, and dynamic content. Handle these explicitly:
// Wait for network response
await page.waitForResponse(resp =>
resp.url().includes('/api/users') && resp.status() === 201
);
// Wait for element to appear after async operation
await expect(page.locator('[data-testid="result"]'))
.toBeVisible({ timeout: 10000 });
// Wait for navigation
await Promise.all([
page.waitForNavigation(),
page.click('[data-testid="submit"]'),
]);
Assert what the user sees, not internal state:
// Good: asserts visible outcome
await expect(page.locator('h1')).toContainText('Dashboard');
await expect(page.locator('[data-testid="user-name"]'))
.toContainText('Test User');
// Bad: asserts internal state
expect(await page.evaluate(() => window.__store.user.isLoggedIn))
.toBe(true);
Run headed (with visible browser) for debugging and development:
npx playwright test --headed
Run headless for continuous integration:
npx playwright test
# Single test file
npx playwright test tests/login.e2e.ts
# Tests matching a pattern
npx playwright test -g "login"
# Specific project/browser
npx playwright test --project=chromium
Use the configured project command or fall back to Playwright's debug tools:
# Debug mode with step-through
npx playwright test --debug
# Generate and view trace
npx playwright test --trace on
npx playwright show-trace trace.zip
# View the HTML report
npx playwright show-report
Configure automatic screenshots for failed tests in playwright.config.ts:
use: {
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
}
Review screenshots to understand the UI state at the moment of failure.
Create a reusable authentication helper to avoid repeating login steps:
async function loginAs(page: Page, role: 'admin' | 'user') {
const credentials = TEST_USERS[role];
await page.goto('/login');
await page.fill('[data-testid="email"]', credentials.email);
await page.fill('[data-testid="password"]', credentials.password);
await page.click('[data-testid="submit"]');
await page.waitForURL('/dashboard');
}
For better performance, use API-based authentication and inject the session cookie directly.
Seed test data via API before tests run:
test.beforeEach(async ({ request }) => {
await request.post('/api/test/seed', {
data: { scenario: 'basic-user-with-items' },
});
});
test.afterEach(async ({ request }) => {
await request.post('/api/test/cleanup');
});
Mock external service responses to avoid flaky tests and third-party dependencies:
await page.route('**/api/external-service/**', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ result: 'mocked-response' }),
});
});
Capture and compare visual snapshots for UI consistency:
await expect(page.locator('[data-testid="chart"]'))
.toHaveScreenshot('dashboard-chart.png');
Update baseline snapshots when intentional UI changes are made:
npx playwright test --update-snapshots
Do not inspect JavaScript variables, store state, or DOM properties that the user cannot see. Test what is visible.
Avoid selectors that break with minor UI changes:
// Bad: fragile selectors
page.locator('.btn-primary.mt-3.mb-2')
page.locator('div > div > form > button:nth-child(2)')
// Good: stable selectors
page.locator('[data-testid="submit-button"]')
page.getByRole('button', { name: 'Submit' })
page.getByLabel('Email address')
Use data-testid attributes or ARIA roles as stable selectors.
Never use fixed sleep or waitForTimeout calls:
// Bad: arbitrary wait
await page.waitForTimeout(3000);
// Good: wait for specific condition
await expect(page.locator('[data-testid="result"]')).toBeVisible();
await page.waitForResponse(resp => resp.url().includes('/api/data'));
Do not write E2E tests for third-party login screens, payment forms, or external services. Mock these at the API boundary.
E2E tests occupy a specific position in the TDD development flow:
Run E2E tests after the unit/integration TDD cycle is complete. They serve as the final confirmation that all pieces work together from the user's perspective.
| Aspect | Guideline |
|---|---|
| When to write | After TDD cycle, before PR submission |
| Structure | Page Object Model with descriptive test names |
| Selectors | data-testid attributes or ARIA roles |
| Assertions | Visible outcomes, not internal state |
| Waits | Condition-based, never hard-coded timeouts |
| Test data | Isolated, seeded via API, cleaned up after |
| External services | Mocked at the API boundary |
| Debugging | Headed mode, traces, screenshots on failure |
| Skill | Reason |
|---|---|
tdd-discipline | E2E tests run after the TDD unit/integration cycle is complete |
| Component | Reason |
|---|---|
playwright-patterns | Builds Playwright-specific API patterns on top of the testing principles defined here |
Agent: e2e-tester | Implements E2E test writing following the patterns defined here |
npx claudepluginhub inteligentsensingsolutions/tdd-dev-workflow --plugin tdd-dev-workflowGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.