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:
Add a
test:e2escript to the rootpackage.jsonConfigure
turbo.jsonto disable caching for E2E tests (they should always run fresh)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:
User clicks “Sign In” on landing page
Django allauth handles login at
/accounts/login/Successful login redirects to the SPA dashboard at
/app/The SPA’s
AuthProviderfetches user data from/api/users/me/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 loginPost-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 appsTheme 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.
Session Storage Reuse (Recommended)
Perform login once at the start of a test run, then save the browser’s storage state. Subsequent tests reuse this state via Playwright’s storageState option. This provides a good balance between speed and realism.
This approach works well with Django’s session-based authentication since session cookies persist across requests.
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:8000Landing app (Astro) at
localhost:5174SPA 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:
Build all applications (parallel builds via Turborepo)
Start services (Django + built frontends)
Run Playwright tests
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-testidattributes for test-specific selectorsAvoid 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
--headedlocally for visual debuggingAdd 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
Testing — Unit testing with pytest (backend)
Type-Safe API Integration — API client generation and type safety
UI Architecture Philosophy — Frontend architecture overview