Playwright Config File Explained
Every building needs a blueprint. Every orchestra needs a conductor. Every Playwright project needs a config file. This single file controls browsers, timeouts, parallelism, retries, and everything about how your tests run.
📖 The Story: The Mystery of the Flaky CI
Imagine a team where tests always passed on laptops but failed randomly on CI. The lead, confused, increased timeouts to 60 seconds. Still flaky. Added --workers=1. Still flaky. Finally, a senior engineer opened playwright.config.ts and found: no retries configured, no base URL set, headless mode mismatched, and screenshots disabled on failure. Five lines of config changes later, the CI was green and stable.
The config file is not bureaucracy—it's your control tower. Every decision about how tests run lives here. Master it, and you control your entire test infrastructure from one file.
🎯 Why the Config File Matters
Single Source of Truth
All settings in one place—no scattered CLI flags or environment variables.
Consistency
Every developer and CI pipeline runs tests with identical configuration.
Multi-Browser
Define Chromium, Firefox, and WebKit projects with device-specific settings.
🌍 Real-World Config File
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ // Where to find tests testDir: './tests', // Global test timeout timeout: 30000, // Assertion timeout expect: { timeout: 5000 }, // Retry failed tests once retries: 1, // Parallel workers workers: process.env.CI ? 2 : 4, // Reporters reporter: 'html', // Shared settings for all tests use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, // Browser projects projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, { name: 'mobile', use: { ...devices['Pixel 5'] } }, ], // Local dev server webServer: { command: 'npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, });
This single file replaces dozens of CLI flags, environment variable hacks, and inconsistent local setups. Every team member runs tests the exact same way.
🧒 Explain Like I'm 10
Think of the config file like the rules of a board game. Before you play Monopoly, everyone agrees: start with $1500, pass Go to collect $200, 4 houses before a hotel. The config file is those rules for your tests: how long before a test "gives up" (timeout), how many times to retry a failing test (retries), and which browsers to play on (projects).
🎓 Professional Explanation
playwright.config.ts uses the defineConfig() helper from @playwright/test to create a typed configuration object. The Playwright test runner reads this file at startup to determine test discovery, execution, and reporting behavior. Configuration follows a cascading override model: global use settings are overridden by project-level use settings, which are overridden by test.use() calls in test files.
📋 Config Options Reference
| Option | Default | What It Controls |
|---|---|---|
testDir | './' | Directory where test files are located |
timeout | 30000 | Max time per test in milliseconds |
expect.timeout | 5000 | Max time for each assertion |
retries | 0 | Number of retries for failed tests |
workers | CPU cores | Number of parallel test workers |
reporter | 'list' | Test output format (html, json, list, dot) |
use.baseURL | — | Base URL for page.goto() relative paths |
use.trace | 'off' | Trace recording (on, off, on-first-retry) |
use.screenshot | 'off' | Screenshot capture (on, off, only-on-failure) |
use.video | 'off' | Video recording (on, off, retain-on-failure) |
use.viewport | 1280×720 | Browser viewport dimensions |
use.storageState | — | Pre-loaded auth cookies/localStorage |
🏠 baseURL: Save Typing, Reduce Errors
Setting baseURL is the single highest-impact config change for new teams. It lets you use relative paths in page.goto():
// In config: baseURL: 'http://localhost:3000' // ❌ Without baseURL — full URL every time await page.goto('http://localhost:3000/login'); await page.goto('http://localhost:3000/dashboard'); // ✅ With baseURL — clean relative paths await page.goto('/login'); await page.goto('/dashboard');
When you change environments (staging → production), you change baseURL in one place instead of editing every test file.
🧪 Projects: Multi-Browser Configuration
Each project in the projects array defines a separate test configuration. Playwright runs all projects by default:
projects: [ // Desktop browsers { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, // Mobile emulation { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } }, { name: 'mobile-safari', use: { ...devices['iPhone 13'] } }, // Branded browser (requires separate install) { name: 'google-chrome', use: { channel: 'chrome' } }, ],
Run a specific project: npx playwright test --project=chromium. This is how CI pipelines run cross-browser tests in parallel across multiple machines.
🔄 Retries & Traces: The Flaky Test Armor
Always set retries: 1 in CI. This gives flaky tests one extra chance without masking real failures. Combine with trace: 'on-first-retry' to capture a full trace only when a test fails the first time—giving you debugging data without the storage cost of tracing every passing test.
🌐 webServer: Auto-Start Your App
webServer: { command: 'npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 120000, },
What this does: Playwright starts your app server before tests and waits for the URL to respond. On CI, it always starts a fresh server. Locally, it reuses an already-running server so you don't have to restart it every time you run tests.
⚠️ Common Mistakes
Mistake 1: Zero Retries in CI
// ❌ Any timing glitch = red build retries: 0, // ✅ Give flaky tests one more chance retries: process.env.CI ? 2 : 0,
Why: In CI, network latency, container cold starts, and resource contention cause intermittent failures. Retrying once or twice separates real bugs from environmental noise.
Mistake 2: Hardcoding Workers
// ❌ Always uses 8 workers — overloads CI workers: 8, // ✅ Scale workers based on environment workers: process.env.CI ? 2 : 4,
✅ Best Practices
- Always set
baseURL— eliminates full URLs in every test. - Use
retries: process.env.CI ? 2 : 0— retry in CI only. - Enable
trace: 'on-first-retry'— debugging data when you need it, no overhead when you don't. - Use
devicespresets — don't manually configure viewport, userAgent, etc. - Separate CI and local settings — use
process.env.CIto conditionally configure. - Keep projects lean in CI — run
chromiumon every PR, full cross-browser on nightly builds.
🏗️ Senior Engineer Deep Dive
Cascading Override Model
Playwright's config follows a strict precedence: CLI flags > test.use() > project use > global use. This means a test.use({ timeout: 10000 }) in a test file overrides the project's use.timeout, which overrides the global timeout. Understanding this cascade prevents confusing "why is my timeout being ignored?" bugs.
StorageState for Authentication
Rather than logging in before every test, extract auth state once and inject it via config:
projects: [ { name: 'authenticated', use: { ...devices['Desktop Chrome'], storageState: '.auth/admin.json', }, }, ],
How to generate the auth file: Create a global-setup.ts that logs in once and saves storageState. Every test in this project starts already authenticated—zero login overhead.
💼 Interview Questions
playwright.config.ts?Show answer
Show answer
Show answer
storageState (cookies + localStorage) to a JSON file. Then reference that file in the project's use.storageState option. Each test in that project starts with auth pre-injected—no login step needed.Show answer
retries: 2 for CI. 2) Enable trace: 'on-first-retry' to capture failure data. 3) Reduce workers — CI containers have fewer resources. 4) Check if webServer timeout is too low for CI cold starts. 5) Verify baseURL points to the correct CI environment.🏋️ Practical Exercises
Customize Your Config
- Open your project's
playwright.config.ts. - Set
baseURLto'https://playwright.dev'. - Update your existing tests to use relative paths like
page.goto('/')instead of full URLs. - Add
trace: 'on-first-retry'andretries: 1. - Run tests with
--project=chromiumto test only Chromium.
Multi-Browser + Mobile Project
- Add 4 projects to your config: chromium, firefox, webkit, and mobile (Pixel 5).
- Run all projects:
npx playwright test. Observe the separate results per project. - Run only mobile:
npx playwright test --project=mobile. - Deliberately use a CSS feature that fails on WebKit. Run
--project=webkitand observe the failure.
🧠 Quiz
createConfig()defineConfig()configure()setup()baseURL allow you to do?page.goto()trace: 'on-first-retry' do?devices['Desktop Chrome'] provide?retries: process.env.CI ? 2 : 0?webServer config option do?npx playwright test --browser=chromiumnpx playwright test --project=chromiumnpx playwright test chromiumnpx playwright test --only=chromiumstorageState in a project's use option do?❓ FAQ
Show answer
playwright.config.js with module.exports = defineConfig({...}). However, TypeScript is recommended because it provides autocompletion and type checking for all config options.Show answer
npm init playwright@latest creates a config file automatically.Show answer
process.env to conditionally set values, or create separate config files and specify which to use: npx playwright test --config=playwright.ci.config.ts.📌 Summary
🎯 Key Takeaways
- defineConfig() creates a typed config object—always use it over raw exports.
- baseURL eliminates full URLs from every test—set it immediately.
- retries: process.env.CI ? 2 : 0 is the production standard for handling CI flakiness.
- trace: 'on-first-retry' captures debugging data only when needed.
- projects define browser/device configurations—use
devicespresets. - webServer auto-starts your app before tests.
- storageState eliminates login steps from every test.
- Cascade order: CLI > test.use() > project use > global use.
