E2e testing
Skill Inteligentsensingsolutions/tdd-dev-workflow/skills/e2e-testing
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.From its SKILL.md
npx -y skills add Inteligentsensingsolutions/tdd-dev-workflow --skill e2e-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
10.5 KB, as published. Nobody here has run it
End-to-End Testing
Purpose
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.
When to Write E2E Tests
Always Write E2E Tests For
- Critical user workflows: Login, registration, data entry, approval flows, payment processing.
- Acceptance criteria involving UI interaction: When the issue specifies user-facing behavior.
- Cross-component integration: Workflows that span multiple pages or modules.
- Regression-prone areas: Features that have broken before.
Timing in the Development Workflow
- Complete the TDD unit/integration test cycle first (Red-Green-Refactor).
- Write E2E tests to validate the full acceptance criteria from the user's perspective.
- Run E2E tests before PR submission.
E2E tests are the final validation layer, not a replacement for unit tests.
Test Structure
Page Object Model
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');
});
Benefits of Page Object Model
- Selectors change in one place, not across dozens of tests.
- Tests read like user stories.
- Reusable actions reduce test code duplication.
Descriptive Test Names
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'
Setup and Teardown
Isolate test data to prevent tests from interfering with each other:
- Create fresh test data before each test or test suite.
- Clean up test data after each test or test suite.
- Never rely on data from other tests.
- Use API calls for setup/teardown instead of UI interactions where possible (faster and more reliable).
Writing E2E Tests
Step 1: Describe the User Journey
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.
Step 2: Map to Page Actions
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');
});
Step 3: Handle Async Operations
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"]'),
]);
Step 4: Assert Visible Outcomes
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);
Running E2E Tests
Development Mode
Run headed (with visible browser) for debugging and development:
npx playwright test --headed
CI Mode
Run headless for continuous integration:
npx playwright test
Running Specific Tests
# 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
Debugging Failures
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
Screenshot on Failure
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.
Common Patterns
Login Helper
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.
Data Seeding
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');
});
API Mocking for External Services
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' }),
});
});
Visual Regression Snapshots
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
Anti-Patterns
Testing Internal State via UI
Do not inspect JavaScript variables, store state, or DOM properties that the user cannot see. Test what is visible.
Flaky Selectors
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.
Hard-Coded Waits
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'));
Testing Third-Party UIs
Do not write E2E tests for third-party login screens, payment forms, or external services. Mock these at the API boundary.
Integration with TDD Workflow
E2E tests occupy a specific position in the TDD development flow:
- Unit tests (TDD cycle): Test individual functions and components in isolation.
- Integration tests (TDD cycle): Test component interactions and API endpoints.
- E2E tests (this skill): Test complete user workflows through the UI.
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.
Quick Reference
| 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 |
Dependencies
| Skill | Reason |
|---|---|
tdd-discipline | E2E tests run after the TDD unit/integration cycle is complete |
Used By
| 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 |