End-to-End Testing
This document describes the end-to-end testing approach used in the Account Overview project. We use Playwright to test complete user journeys across the application.
Overview
End-to-end (E2E) tests verify that the application works correctly from a user's perspective by testing complete workflows. Unlike unit tests that focus on individual components, E2E tests validate entire user journeys.
Testing Stack
- Playwright - Browser automation and testing framework
- MSW (Mock Service Worker) - API mocking at the network level
- @axe-core/playwright - Accessibility testing (separate test suite)
Configuration
Tests are configured in playwright.config.ts:
- Test directory:
./tests/e2e/ - Base URL:
http://localhost:3000/ - Browsers: Chromium, Firefox, WebKit
- Parallel execution: Enabled locally, single worker in CI
- Retries: 2 attempts in CI for reliability
- Traces: Captured on first retry for debugging
Project Structure
tests/
├── e2e/ # End-to-end test suites
│ ├── helpers.ts # Shared utilities
│ └── *.test.ts # Test files
├── visual/ # Visual regression tests
├── accessibility/ # Accessibility tests
└── mocks/ # Mock server setup
├── server.ts
└── handlers/
Testing Approach
Philosophy
E2E tests focus on complete user journeys rather than isolated features. A good E2E test follows the path a real user would take through the application.
Key principles:
- Test complete workflows from start to finish
- Use realistic test data and scenarios
- Follow sequential user actions
- Maintain user context throughout the flow
- Test from the user's entry point
What We Test
Complete user journeys:
- Registration and onboarding flows
- Login and authentication
- Product discovery and comparison
- Purchase and upgrade flows
- Account management
Page functionality:
- Text content and headings
- Navigation and links
- Button labels and interactions
- Form submissions
- Success and error messages
What we avoid testing:
- Images and graphics
- Styling and visual design
- Animations and transitions
- Analytics and tracking code (GTM)
- Meta tags and SEO elements
- Responsive layouts
- Implementation details
Selector Strategy
Selectors match how users identify elements, prioritized as follows:
-
Role-based selectors (preferred):
page.getByRole("button", { name: "Login" }); page.getByRole("heading", { name: "Welcome" }); -
Label-based selectors:
page.getByLabel("Email address"); page.getByLabel("Password"); -
Text content selectors:
page.getByText("You are logged in"); -
Attribute selectors (use sparingly):
page.fill("[name=email]", "user@statista.com");
Note: The project doesn't use data-testid attributes. All selectors should be semantic and user-focused.
User Context Management
Tests simulate real user contexts using cookies:
test.beforeEach(async ({ context }) => {
// Set geographic location
await addCookie(context, "uao-iso-country", "US");
// Enable feature toggles
await addCookie(context, "uao-toggles", "new-feature");
// Set platform
await addCookie(context, "uao-platform", "web");
});
test.afterEach(async ({ context }) => {
await context.clearCookies();
});
Common Patterns
Basic User Journey Test
import { expect, test } from "@playwright/test";
import { href } from "react-router";
import { addCookie } from "./helpers.js";
test.describe("User Registration", () => {
test.beforeEach(async ({ context }) => {
await addCookie(context, "uao-iso-country", "US");
});
test.afterEach(async ({ context }) => {
await context.clearCookies();
});
test("new user completes registration", async ({ page }) => {
await page.goto(route("/"));
await page.getByRole("button", { name: "Get Started" }).click();
await expect(page).toHaveURL(/register/);
await page.getByLabel("Email").fill("user@statista.com");
await page.getByLabel("Password").fill("SecurePass123!");
await page.getByRole("button", { name: "Create Account" }).click();
await expect(page).toHaveURL(/success/);
await expect(page.getByText("Welcome to Statista")).toBeVisible();
});
});
Multi-Step Workflows
test("user discovers, compares, and purchases product", async ({ page }) => {
// Step 1: Discover product
await page.goto(route("/products"));
await expect(
page.getByRole("heading", { name: "Our Products" }),
).toBeVisible();
// Step 2: View details
await page.getByRole("link", { name: "Premium Account" }).click();
await expect(page.getByText("Premium Features")).toBeVisible();
// Step 3: Initiate purchase
await page.getByRole("button", { name: "Buy Now" }).click();
await expect(page).toHaveURL(/checkout/);
// Step 4: Complete purchase
await page.getByLabel("Card number").fill("4242424242424242");
await page.getByRole("button", { name: "Complete Purchase" }).click();
// Verify success
await expect(page.getByText("Purchase successful")).toBeVisible();
});
Form Interactions
import { fillRegistrationForm } from "./helpers.js";
test("user fills out registration form", async ({ page }) => {
await page.goto(route("/register"));
await fillRegistrationForm(page, {
email: "test@statista.com",
firstName: "John",
lastName: "Doe",
});
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Registration successful")).toBeVisible();
});
Authentication Flows
import { handleLogin } from "./helpers.js";
test("returning user logs in", async ({ page }) => {
await page.goto(route("/login"));
await handleLogin(page, {
email: "existing@statista.com",
password: "SecurePass123!",
});
await expect(page).toHaveURL(route("/dashboard"));
await expect(page.getByText("Welcome back")).toBeVisible();
});
Navigation and URL Verification
import { getPathname } from "./helpers.js";
test("user navigates through main sections", async ({ page }) => {
await page.goto(route("/"));
await page.getByRole("link", { name: "Products" }).click();
await page.waitForURL(/products/);
expect(getPathname(page.url())).toContain("/products");
await page.getByRole("link", { name: "Pricing" }).click();
await page.waitForURL(/pricing/);
expect(getPathname(page.url())).toContain("/pricing");
});
Error Handling
test("user sees validation error for invalid email", async ({ page }) => {
await page.goto(route("/register"));
await page.getByLabel("Email").fill("invalid-email");
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Please enter a valid email")).toBeVisible();
await expect(page).toHaveURL(/register/); // Still on registration page
});
Best Practices
Test Organization
Tests are grouped by user journeys rather than technical features:
test.describe("New User Onboarding Journey", () => {
// Multiple related tests for new user flow
});
test.describe("Account Upgrade Journey", () => {
// Tests for upgrade workflow
});
Wait Strategies
Playwright automatically waits for elements to be actionable, but explicit waits are sometimes needed:
// Navigation
await page.waitForURL(/success/);
// Network idle (for visual tests)
await page.goto(route("/"), { waitUntil: "networkidle" });
// Specific condition
await page.waitForSelector('[aria-label="Loading complete"]');
Cookie Management
Always clean up cookies between tests:
test.afterEach(async ({ context }) => {
await context.clearCookies();
});
Test Isolation
Each test should be independent and able to run in any order:
- Create necessary data within the test
- Clean up state after the test
- Don't depend on other tests running first
Helper Functions
The project provides helper functions in tests/e2e/helpers.ts:
addCookie()- Set cookies for feature toggles and locationfillRegistrationForm()- Fill registration forms consistentlyhandleLogin()- Perform login actionsroute()- Generate consistent URLsgetPathname()- Extract pathname from full URL
Running Tests
Available commands:
# Run E2E tests
pnpm run integration
# Run with UI for debugging
pnpm run integration --headed
# Run specific test file
pnpm run integration tests/e2e/registration.test.ts
# Run in debug mode
pnpm run integration --debug
# Cross-browser testing
pnpm run integration:all
# Visual regression tests
pnpm run integration:visual
# Accessibility tests
pnpm run integration:a11y
CI Integration
Tests run automatically in CI with these configurations:
- Single worker (no parallel execution)
- 2 retry attempts for flaky test resilience
- Automatic trace capture on failures
- Screenshot capture on failures
- GitHub Actions reporter for PR status checks
Environment variables are set in playwright.config.ts to enable mocking and configure the application.
Debugging
Local Debugging
# Run with visible browser
pnpm run integration --headed
# Step through tests
pnpm run integration --debug
# Run with slow motion
pnpm run integration --headed --slow-mo=1000
CI Debugging
When tests fail in CI:
- Check the HTML report artifact
- Review captured traces
- Examine failure screenshots
- Check console output in test logs
Common Issues
Flaky tests:
- Use proper wait strategies instead of fixed timeouts
- Ensure stable selectors that don't depend on dynamic content
- Clean up cookies and state between tests
Timeout errors:
- Increase timeout for slow operations
- Check if page is actually loading
- Verify mock data is being served correctly
Test Data
All test data uses realistic values:
- Email addresses:
user@statista.com(neverexample.com) - Names: Realistic first and last names
- Dates: Valid date formats
- URLs: Real or realistic paths
API Mocking
API requests are mocked using MSW, configured in tests/mocks/server.ts. Tests can add custom handlers as needed:
import { server } from "../mocks/server.js";
import { http, HttpResponse } from "msw";
test("handles API error gracefully", async ({ page }) => {
server.use(
http.get("/api/user", () => {
return HttpResponse.json({ error: "Not found" }, { status: 404 });
}),
);
await page.goto(route("/profile"));
await expect(page.getByText("Profile not found")).toBeVisible();
});
Further Reading
- Playwright Documentation
- Unit Testing Guide for component-level testing
- Playwright Best Practices