Skip to main content

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

  1. Use page objects - Organize selectors and actions
  2. Wait for elements - Don't use fixed timeouts
  3. Test user perspective - Test what users see and do
  4. Keep tests independent - Each test should work alone
  5. Clean up test data - Remove data created during tests
  6. 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

  1. Use headed mode - See what's happening
  2. Use debug mode - Step through test
  3. Add console logs - Log page state
  4. Take screenshots - See what page looks like
  5. Use Playwright Inspector - Interactive debugging

Resources