E2E Testing Skill
Purpose: Comprehensive end-to-end testing framework using Puppeteer for frontend, middleware, and backend testing.
When to use: When you need to validate complete user workflows, API integrations, or full-stack functionality across the entire application stack.
Overview
This skill provides Puppeteer-based E2E testing templates and utilities for testing at three critical layers:
- •Frontend E2E: User interface interactions, forms, navigation, accessibility
- •Middleware E2E: API endpoints, authentication, session management, data transformation
- •Backend E2E: Complete workflows, data persistence, third-party integrations
Test Pyramid Position
E2E tests sit at the top of the test pyramid:
- •Volume: 10% of total tests
- •Speed: Slowest (seconds to minutes)
- •Scope: Widest (entire stack)
- •Cost: Highest (complex setup, maintenance)
Quick Start
Installation
# Install dependencies npm install --save-dev puppeteer jest-puppeteer # Or with specific Puppeteer version npm install --save-dev puppeteer@21.6.1 jest-puppeteer
Configuration
Create jest-puppeteer.config.js:
module.exports = {
launch: {
headless: true,
slowMo: 50,
defaultViewport: {
width: 1920,
height: 1080
},
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage'
]
},
browserContext: 'default',
exitOnPageError: false
};
Update jest.config.js:
module.exports = {
preset: 'jest-puppeteer',
testMatch: ['**/e2e/**/*.test.js'],
testTimeout: 30000,
setupFilesAfterEnv: ['<rootDir>/e2e/setup.js']
};
Usage
Generate Test from Template
# Frontend E2E test cp .claude/skills/e2e-testing/templates/frontend-e2e.template.js \ tests/e2e/login-flow.test.js # Middleware E2E test cp .claude/skills/e2e-testing/templates/middleware-e2e.template.js \ tests/e2e/api-auth.test.js # Backend E2E test cp .claude/skills/e2e-testing/templates/backend-e2e.template.js \ tests/e2e/user-registration.test.js
Run Tests
# Run all E2E tests npm run test:e2e # Run with headed browser (see what's happening) npm run test:e2e:headed # Run specific test file npm test tests/e2e/login-flow.test.js # Run with debugging PWDEBUG=1 npm test tests/e2e/login-flow.test.js
Templates
1. Frontend E2E Template
File: templates/frontend-e2e.template.js
Use for:
- •User login/logout flows
- •Form submissions and validation
- •Navigation and routing
- •UI state changes
- •Accessibility testing
- •Responsive design testing
Example:
describe('Login Flow', () => {
test('should login successfully with valid credentials', async () => {
await page.goto('http://localhost:3000/login');
await page.type('#email', 'user@example.com');
await page.type('#password', 'SecurePass123!');
await page.click('button[type="submit"]');
await page.waitForNavigation();
expect(page.url()).toBe('http://localhost:3000/dashboard');
});
});
2. Middleware E2E Template
File: templates/middleware-e2e.template.js
Use for:
- •API endpoint testing
- •Authentication flows
- •Session management
- •Rate limiting
- •CORS validation
- •Error handling
Example:
describe('API Authentication Middleware', () => {
test('should reject request without auth token', async () => {
const response = await page.evaluate(async () => {
const res = await fetch('http://localhost:3000/api/protected', {
method: 'GET'
});
return { status: res.status, body: await res.json() };
});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Unauthorized');
});
});
3. Backend E2E Template
File: templates/backend-e2e.template.js
Use for:
- •Complete user workflows
- •Multi-step processes
- •Data persistence validation
- •Third-party integrations
- •Email/SMS verification
- •Payment processing
Example:
describe('User Registration Workflow', () => {
test('should complete full registration flow', async () => {
// 1. Register
await page.goto('http://localhost:3000/register');
await fillRegistrationForm(page);
await page.click('button[type="submit"]');
// 2. Verify email sent (check test inbox)
const verificationLink = await getVerificationEmail();
await page.goto(verificationLink);
// 3. Verify account created in database
const user = await db.findUserByEmail('newuser@example.com');
expect(user.verified).toBe(true);
// 4. Login with new account
await loginAsUser(page, 'newuser@example.com', 'password123');
expect(page.url()).toContain('/dashboard');
});
});
Best Practices
1. Test Organization
tests/
└── e2e/
├── setup.js # Global setup
├── teardown.js # Global teardown
├── helpers/ # Reusable helpers
│ ├── auth.js
│ ├── database.js
│ └── navigation.js
├── fixtures/ # Test data
│ └── users.json
└── tests/
├── frontend/ # UI tests
│ ├── login.test.js
│ └── checkout.test.js
├── middleware/ # API tests
│ ├── auth.test.js
│ └── rate-limit.test.js
└── backend/ # Full-stack tests
├── registration.test.js
└── payment.test.js
2. Selectors Strategy
Prefer data attributes:
// Good - stable, semantic
await page.click('[data-testid="login-button"]');
// Okay - if unique and stable
await page.click('#login-button');
// Avoid - brittle, implementation-dependent
await page.click('.btn.btn-primary.mt-4');
3. Waiting Strategy
// ✅ Wait for navigation
await page.click('a[href="/dashboard"]');
await page.waitForNavigation();
// ✅ Wait for element
await page.waitForSelector('[data-testid="dashboard-content"]');
// ✅ Wait for network idle
await page.goto(url, { waitUntil: 'networkidle2' });
// ✅ Wait for specific condition
await page.waitForFunction(() => {
return document.querySelector('.spinner') === null;
});
// ❌ Avoid arbitrary waits
await page.waitForTimeout(3000); // Flaky!
4. Error Handling
test('should handle network errors gracefully', async () => {
// Simulate network failure
await page.setOfflineMode(true);
await page.click('[data-testid="submit-button"]');
// Should show error message
const errorMessage = await page.waitForSelector('.error-message');
expect(await errorMessage.textContent()).toContain('Network error');
// Restore connection
await page.setOfflineMode(false);
});
5. Test Data Management
// ✅ Create fresh data for each test
beforeEach(async () => {
await db.seed.testUsers();
});
afterEach(async () => {
await db.clean.testUsers();
});
// ✅ Use factories
const user = await createTestUser({
email: 'test@example.com',
role: 'admin'
});
// ❌ Avoid shared state between tests
let globalUser; // Don't do this!
Common Patterns
Authentication Helper
// e2e/helpers/auth.js
export async function loginAsUser(page, email, password) {
await page.goto('http://localhost:3000/login');
await page.type('[data-testid="email-input"]', email);
await page.type('[data-testid="password-input"]', password);
await page.click('[data-testid="login-button"]');
await page.waitForNavigation();
}
export async function getAuthToken(page) {
return page.evaluate(() => localStorage.getItem('authToken'));
}
Form Filling Helper
// e2e/helpers/forms.js
export async function fillForm(page, formData) {
for (const [fieldName, value] of Object.entries(formData)) {
const selector = `[name="${fieldName}"]`;
await page.waitForSelector(selector);
await page.type(selector, value);
}
}
// Usage
await fillForm(page, {
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
});
Screenshot on Failure
// e2e/setup.js
afterEach(async () => {
if (global.testResult?.status === 'failed') {
const testName = expect.getState().currentTestName;
const screenshotPath = `screenshots/${testName}.png`;
await page.screenshot({ path: screenshotPath, fullPage: true });
console.log(`Screenshot saved: ${screenshotPath}`);
}
});
API Mocking
test('should display products from API', async () => {
// Intercept API call and return mock data
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().includes('/api/products')) {
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Product 1', price: 29.99 },
{ id: 2, name: 'Product 2', price: 39.99 }
])
});
} else {
request.continue();
}
});
await page.goto('http://localhost:3000/products');
const products = await page.$$('[data-testid="product-card"]');
expect(products).toHaveLength(2);
});
Performance Optimization
1. Parallel Execution
// jest.config.js
module.exports = {
maxWorkers: 4, // Run 4 tests in parallel
// or
maxWorkers: '50%' // Use 50% of CPU cores
};
2. Reuse Browser Context
// e2e/setup.js
let browser;
let context;
beforeAll(async () => {
browser = await puppeteer.launch();
context = await browser.createIncognitoBrowserContext();
});
beforeEach(async () => {
page = await context.newPage();
});
afterEach(async () => {
await page.close();
});
afterAll(async () => {
await context.close();
await browser.close();
});
3. Resource Blocking
// Block unnecessary resources to speed up tests
await page.setRequestInterception(true);
page.on('request', request => {
const blockedResources = ['image', 'stylesheet', 'font'];
if (blockedResources.includes(request.resourceType())) {
request.abort();
} else {
request.continue();
}
});
Debugging
1. Headed Mode
# See the browser while tests run HEADLESS=false npm test
2. Slow Motion
// jest-puppeteer.config.js
module.exports = {
launch: {
headless: false,
slowMo: 100 // Slow down by 100ms per action
}
};
3. DevTools
// Pause execution and open DevTools
await page.evaluate(() => {
debugger;
});
4. Console Logs
// Capture browser console logs
page.on('console', msg => console.log('BROWSER LOG:', msg.text()));
CI/CD Integration
GitHub Actions
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Start application
run: npm start &
- name: Wait for app
run: npx wait-on http://localhost:3000
- name: Run E2E tests
run: npm run test:e2e
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: screenshots
path: screenshots/
Scripts
Test Runner
File: scripts/run-e2e-tests.sh
#!/bin/bash # Comprehensive E2E test runner with environment setup ./scripts/run-e2e-tests.sh --env=staging --headed
Examples
See examples/ directory for complete working examples:
- •
examples/frontend-login.test.js- Complete login flow test - •
examples/middleware-api-auth.test.js- API authentication test - •
examples/backend-checkout.test.js- Full checkout workflow - •
examples/accessibility.test.js- Accessibility testing - •
examples/visual-regression.test.js- Visual regression testing
Troubleshooting
Issue: Tests timeout
Solution:
// Increase timeout for slow operations
test('slow operation', async () => {
// ... test code
}, 60000); // 60 second timeout
// Or globally in jest.config.js
module.exports = {
testTimeout: 60000
};
Issue: Element not found
Solution:
// Wait for element before interacting
await page.waitForSelector('[data-testid="button"]', {
visible: true,
timeout: 5000
});
await page.click('[data-testid="button"]');
Issue: Tests fail in CI but pass locally
Solution:
// Ensure consistent viewport
await page.setViewport({
width: 1920,
height: 1080
});
// Wait for network idle
await page.goto(url, { waitUntil: 'networkidle2' });
// Add explicit waits
await page.waitForSelector('[data-testid="content"]');
Related Skills
- •
/testing-strategies- Overall testing strategy and test pyramid - •
/code-review- Review E2E test code quality - •
/deployment-patterns- Run E2E tests as part of deployment smoke tests
Resources
- •Puppeteer Documentation
- •Jest-Puppeteer
- •Puppeteer Recorder - Chrome extension to record interactions
Remember: E2E tests are expensive. Use them for critical user journeys only. Cover edge cases with unit and integration tests.