Planning an E2E Suite for a Real Application
Before writing a single test, identify the application's critical user journeys, the small set of flows (sign up, checkout, core dashboard actions) that generate the most business value or represent the highest risk if broken. E2E tests are expensive to write and slow to run compared to unit tests, so a risk-based approach that covers these journeys thoroughly, while leaving edge-case validation to lower-level unit and integration tests, keeps the suite fast and high-signal rather than bloated with redundant coverage.
Cricket analogy: A limited-overs captain sets a field to protect the boundaries most likely to be attacked rather than guarding every blade of grass equally, just as an E2E suite focuses on the highest-risk user journeys rather than testing every minor page.
Project Structure and Config
A real-world Playwright project centers on playwright.config.ts, which defines baseURL so tests can call page.goto('/login') instead of full URLs, the projects array for running the same suite against multiple browsers (chromium, firefox, webkit), and shared use options like viewport and trace settings. Tests are typically organized under tests/ mirroring the app's feature areas (tests/checkout/, tests/auth/), with shared page objects and fixtures kept in a separate directory so they can be imported consistently across spec files.
Cricket analogy: A cricket board's fixture list is organized by format, Test, ODI, T20, each with its own rules and venues, just as playwright.config.ts organizes 'projects' by browser engine, each with its own settings.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['html', { open: 'never' }]],
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});Test Data and Environment Management
Rather than clicking through the UI to create prerequisite data (a product to add to cart, an existing user to log in as), real suites seed state directly through the app's API using page.request or a dedicated setup script, which is far faster and less flaky than driving the UI for setup steps that aren't the point of the test. For authentication specifically, Playwright's storageState mechanism lets a one-time login flow save cookies and localStorage to a JSON file, which every subsequent test loads instantly via the storageState config option, avoiding a slow UI login before every single test.
Cricket analogy: A ground crew prepares the pitch and sets up stumps well before play starts rather than doing it live during the match, mirroring how seeding test data via API happens before the test's real actions begin.
Set up storageState with a dedicated auth.setup.ts project that logs in once and saves the state to playwright/.auth/user.json. Reference it via storageState in other projects' use block, so every dependent test starts already authenticated, dramatically speeding up the suite.
CI Integration and Parallelization
In CI, Playwright tests should run with sharding (--shard=1/4) across multiple machines or jobs to keep wall-clock time low as the suite grows, combined with the workers option to parallelize within each shard. A small number of automatic retries (retries: 2 in CI) helps absorb genuine environmental flakiness like a slow network blip, but retries should never be used to paper over a test that is fundamentally unreliable; the trace: 'on-first-retry' setting captures a full trace only when a test actually fails and retries, giving debugging information without the overhead of tracing every passing run.
Cricket analogy: A tournament schedules multiple matches simultaneously across different grounds rather than playing every fixture sequentially on one pitch, mirroring how CI sharding splits tests across machines to finish faster.
Retries mask flakiness, they don't fix it. If a test only passes on its second or third attempt, treat that as a signal to investigate a race condition or missing wait condition, not as an acceptable steady state. Track retry rates over time and fix consistently-retrying tests before they erode trust in the suite.
- Prioritize E2E coverage around critical, high-risk user journeys rather than exhaustive UI coverage.
- Centralize configuration in playwright.config.ts, including baseURL and multi-browser projects.
- Seed test data via API calls instead of driving the UI for setup steps.
- Use storageState to persist authenticated sessions and avoid a slow UI login before every test.
- Shard and parallelize tests in CI to keep wall-clock time manageable as the suite grows.
- Use limited automatic retries for genuine flakiness, not as a fix for unreliable tests.
- Capture traces on first retry to get debugging context without tracing every passing run.
Practice what you learned
1. What is the recommended approach for selecting which flows to cover with E2E tests?
2. What does the baseURL option in playwright.config.ts allow tests to do?
3. Why is seeding test data via API generally preferred over driving the UI to create prerequisite state?
4. What problem does storageState solve?
5. What is the appropriate use of retries in CI according to best practice?
Was this page helpful?
You May Also Like
Playwright Best Practices
A practical guide to writing reliable, maintainable Playwright tests by choosing the right locators, avoiding flaky waits, and keeping tests isolated and fast.
Page Object Model in Playwright
How to structure Playwright tests using the Page Object Model to separate UI locators and actions from test logic for better maintainability.
Playwright Quick Reference
A condensed cheat sheet of the most-used Playwright locators, assertions, actions, and CLI commands for day-to-day test writing.