End-to-End Testing
E2E tests verify complete user workflows in a real browser environment. We use Playwright for E2E testing.
What to Test
✅ DO Test
- Critical user flows (signup, login, checkout)
- Complete workflows (onboarding, task creation)
- User interactions (forms, navigation)
- Authentication flows
- Payment flows
❌ DON'T Test
- Individual UI components (test in unit tests)
- API endpoints (test in integration tests)
- Business logic (test in unit tests)
Writing E2E Tests
Example: Testing User Signup Flow
// tests/e2e/signup.test.js
import { test, expect } from '@playwright/test';
test.describe('User Signup', () => {
test('complete signup flow', async ({ page }) => {
// Navigate to signup page
await page.goto('https://reformer.la/signup');
// Fill signup form
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'SecurePassword123!');
await page.fill('input[name="companyName"]', 'Test Company');
await page.fill('input[name="firstName"]', 'Test');
await page.fill('input[name="lastName"]', 'User');
// Submit form
await page.click('button[type="submit"]');
// Wait for redirect to dashboard
await page.waitForURL('**/dashboard', { timeout: 10000 });
// Verify dashboard loaded
await expect(page.locator('h1')).toContainText('Welcome');
});
test('shows validation errors', async ({ page }) => {
await page.goto('https://reformer.la/signup');
// Try to submit empty form
await page.click('button[type="submit"]');
// Verify validation errors shown
await expect(page.locator('.error-message')).toContainText('required');
});
});
Example: Testing Authentication
// tests/e2e/auth.test.js
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test('user can login', async ({ page }) => {
await page.goto('https://reformer.la/login');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('redirects to login when not authenticated', async ({ page }) => {
// Try to access protected page
await page.goto('https://reformer.la/dashboard');
// Should redirect to login
await page.waitForURL('**/login');
});
});
Best Practices
- Use page objects - Organize selectors and actions
- Wait for elements - Don't use fixed timeouts
- Test user perspective - Test what users see and do
- Keep tests independent - Each test should work alone
- Clean up test data - Remove data created during tests
- Use realistic data - Test with production-like scenarios
Page Object Pattern
// tests/e2e/page-objects/SignupPage.js
export class SignupPage {
constructor(page) {
this.page = page;
this.emailInput = page.locator('input[name="email"]');
this.passwordInput = page.locator('input[name="password"]');
this.submitButton = page.locator('button[type="submit"]');
}
async goto() {
await this.page.goto('https://reformer.la/signup');
}
async signup(email, password, companyName) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.page.fill('input[name="companyName"]', companyName);
await this.submitButton.click();
}
}
// tests/e2e/signup.test.js
import { test, expect } from '@playwright/test';
import { SignupPage } from './page-objects/SignupPage.js';
test('signup with page object', async ({ page }) => {
const signupPage = new SignupPage(page);
await signupPage.goto();
await signupPage.signup('test@example.com', 'password123', 'Test Company');
await expect(page).toHaveURL('**/dashboard');
});
Running E2E Tests
# Run all E2E tests
npm run test:e2e
# Run specific test file
npm run test:e2e -- signup.test.js
# Run in headed mode (see browser)
npm run test:e2e:headed
# Run in debug mode
npm run test:e2e:debug
# Run with UI mode
npm run test:e2e:ui
Test Environment
Staging Environment
E2E tests should run against staging:
- Production-like data
- Real integrations (test mode)
- Stable environment
Test Data
// Use fixtures for test data
import { test } from '@playwright/test';
test.use({
// Set test user credentials
storageState: 'tests/fixtures/auth-state.json'
});
Common Patterns
Waiting for Elements
// ✅ GOOD: Wait for element
await page.waitForSelector('.dashboard');
await expect(page.locator('.dashboard')).toBeVisible();
// ❌ BAD: Fixed timeout
await page.waitForTimeout(5000);
Handling Async Operations
// Wait for API call to complete
await page.waitForResponse(response =>
response.url().includes('/api/users') &&
response.status() === 201
);
Taking Screenshots
// Take screenshot on failure
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== 'passed') {
await page.screenshot({ path: `screenshots/${testInfo.title}.png` });
}
});
Debugging E2E Tests
- Use headed mode - See what's happening
- Use debug mode - Step through test
- Add console logs - Log page state
- Take screenshots - See what page looks like
- Use Playwright Inspector - Interactive debugging