E2E Testing with Playwright

End-to-end (E2E) tests verify complete user journeys across your application stack. While unit tests (see Testing) validate individual components in isolation, E2E tests ensure that the Landing site, Django backend, and React SPA work together correctly.

In a monorepo with multiple frontend applications and a Django backend, E2E tests are particularly valuable for validating integration points: authentication flows, API interactions, and cross-app navigation.

When to Use E2E Tests

E2E tests are slow and expensive compared to unit tests. Use them strategically for:

  • Critical user flows: Login, signup, checkout, core business operations

  • Cross-app transitions: Landing page to authenticated app, logout flows

  • Integration verification: Frontend and backend working together correctly

Prefer unit and integration tests for edge cases, error handling, and business logic validation.

Where Playwright Fits in the Monorepo

The recommended approach is to create a dedicated packages/e2e/ workspace package that tests across all applications:

packages/
└── e2e/
    ├── package.json
    ├── playwright.config.ts
    ├── tests/
    │   ├── auth/           # Authentication flows
    │   ├── landing/        # Landing page tests
    │   ├── app/            # SPA tests
    │   └── cross-app/      # Tests spanning applications
    └── fixtures/           # Shared test utilities

This structure keeps E2E tests separate from application code while allowing them to test the full stack. The alternative—placing tests within each app—makes cross-app flow testing more difficult and duplicates configuration.

Turborepo Integration

Add E2E testing to your Turborepo workflow:

  1. Add a test:e2e script to the root package.json

  2. Configure turbo.json to disable caching for E2E tests (they should always run fresh)

  3. Ensure E2E tests depend on both frontend apps being built

Key User Flows to Test

Focus E2E tests on the flows that matter most to your users and business.

Authentication Flows

The authentication flow spans Django and the React SPA:

  1. User clicks “Sign In” on landing page

  2. Django allauth handles login at /accounts/login/

  3. Successful login redirects to the SPA dashboard at /app/

  4. The SPA’s AuthProvider fetches user data from /api/users/me/

  5. Dashboard renders with user information

Test both the happy path and failure scenarios (invalid credentials, session expiry).

Landing to App Transitions

  • Marketing pages render correctly

  • Call-to-action buttons navigate to authentication pages

  • Unauthenticated access to /app/ redirects to login

  • Post-login redirect returns users to their originally requested page

SPA Functionality

  • Dashboard loads and displays user data from the API

  • Theme toggle persists user preference

  • Navigation between SPA routes works correctly

  • Error states display appropriately when API calls fail

Cross-App Consistency

  • Navigation between landing site and authenticated app works smoothly

  • Shared UI components from packages/ui/ render consistently across apps

  • Theme settings persist across applications

Handling Authentication in E2E Tests

Most E2E tests require an authenticated user. There are three approaches, each with trade-offs.

Full Login Flow

Playwright fills the login form and submits it for each test. This is the most realistic but slowest approach. Use it for tests that specifically validate authentication behavior.

API-Based Authentication

Hit Django’s authentication endpoint directly and set cookies programmatically. This is the fastest option but bypasses the frontend authentication code entirely. Use it for tests that don’t focus on authentication.

Django-Specific Considerations

  • Django requires CSRF tokens for POST requests—ensure your test setup handles this

  • Session cookies are set by Django allauth at /accounts/

  • For multi-domain setups, configure cookies to work across subdomains

Running Tests Against Different Environments

Local Development

During local development, all services run via docker compose:

  • Django backend at localhost:8000

  • Landing app (Astro) at localhost:5174

  • SPA app (React) at localhost:5173

Use Playwright’s webServer configuration to start services before tests run, or run them manually with Docker Compose.

Staging and Production

For staging environments, configure Playwright to point at deployed URLs using environment variables:

// playwright.config.ts (conceptual)
const config = {
  use: {
    baseURL: process.env.APP_URL || 'http://localhost:5173',
  },
};

Consider creating a dedicated test organization or user in staging environments for E2E tests. Avoid tests that depend on specific production data.

Test Data Management

  • Use Django’s Factory Boy to create test fixtures

  • Consider a dedicated test database for E2E runs

  • Clean up test data after test runs to avoid state pollution

  • Keep tests independent—each test should set up its own required state

CI/CD Integration

E2E tests should run in your continuous integration pipeline to catch integration issues before deployment.

GitHub Actions Workflow

A typical workflow structure:

  1. Build all applications (parallel builds via Turborepo)

  2. Start services (Django + built frontends)

  3. Run Playwright tests

  4. Upload artifacts on failure (screenshots, traces)

When to Run E2E Tests

  • On PRs: Run a smoke test subset for quick feedback

  • On merge to main: Run the full E2E suite

  • Before deployments: Gate production deployments on E2E success

  • Nightly: Run comprehensive regression tests

Artifacts to Preserve

Configure your CI to save these artifacts when tests fail:

  • Screenshots: Visual state at failure point

  • Traces: Playwright’s trace viewer data for debugging

  • Videos: Optional, useful but storage-intensive

Best Practices

Test Organization

  • Group tests by user journey, not by application or component

  • Use the Page Object Model pattern for maintainability

  • Share fixtures for common operations (login, navigation)

  • Tag tests by criticality (@critical, @smoke) for selective runs

Selector Strategy

  • Use data-testid attributes for test-specific selectors

  • Avoid selectors tied to implementation details (CSS classes, DOM structure)

  • Prefer accessible selectors (roles, labels) when available

Performance

  • Limit E2E tests to critical paths—don’t duplicate unit test coverage

  • Use parallel test execution for independent test groups

  • Mock external services (payment providers, third-party APIs) to avoid flakiness

  • Test against built output in CI, not development servers

Debugging Failures

  • Use Playwright’s trace viewer for CI failures

  • Run with --headed locally for visual debugging

  • Add meaningful test names and failure messages

  • Use test.step() to document complex test flows

Anti-Patterns to Avoid

  • Testing implementation details that may change

  • Tests that depend on execution order

  • Hard-coded waits instead of explicit waits for elements/conditions

  • Flaky tests that sometimes pass and sometimes fail

See Also