# Playwright Configuration

Course: [Playwright](https://www.devtutorials.ai/learn/playwright) · Section: Test Structure and Configuration
Source: https://www.devtutorials.ai/learn/playwright/test-structure/playwright-configuration

> playwright.config.ts and defineConfig, core options (testDir, testMatch, outputDir, forbidOnly, shared use options, test.use), projects for browsers, branded channels, dependencies and setup projects, the webServer option, every kind of timeout, workers and parallel mode, retries and flaky tests, environment variables with dotenv, command-line overrides, and global setup.

## Playwright Configuration

- `playwright.config.ts` decides **how** your tests run: where they are, which browsers, how long to wait, how many at once.
- You change behaviour for every test in one place, without editing the tests.
- In this lesson, every lab has an **editable** config. Open `playwright.config.ts` in the explorer, change it, and run again.

**Simple lesson:** the config file controls how all your tests run.

### A Real-World Example

- Priya's tests repeat `https://practice.devtutorials.ai` in every `goto()`. They run in one browser only.
- She sets `baseURL` once, and adds a Firefox project. Now every test is shorter and runs twice.

- Try it: add a `webkit` project with `devices['Desktop Safari']`, then run again.

### What You Will Learn

1. The basics: `defineConfig()` and where the file lives.
2. Core options, and `use` options shared by all tests.
3. Projects: browsers, devices, dependencies and setup projects.
4. Starting your app with `webServer`.
5. Timeouts, parallel runs and retries.
6. Environment variables and command-line overrides.
7. Global setup (advanced).

## Configuration Basics

### playwright.config.ts and defineConfig()

- The config is a TypeScript (or JavaScript) file that **exports** settings.
- `defineConfig({ ... })` wraps them, so your editor knows every option and its type.

### Config File Locations

- Playwright looks for `playwright.config.ts` (or `.js`, `.mjs`) in the folder where you run it.
- Use another file with `npx playwright test --config=other.config.ts`.

**Simple lesson:** one `defineConfig({ ... })` file, next to `package.json`, sets up every run.

## Core Options

### testDir, testMatch and testIgnore

- `testDir: './tests'` is the folder to search.
- `testMatch` says which files are tests. The default matches `.spec` and `.test` files.
- `testIgnore` skips files, for example `'**/drafts/**'`.

### outputDir and forbidOnly

- `outputDir` is where traces, screenshots and videos go. Default: `test-results`.
- `forbidOnly: !!process.env.CI` fails the run if `test.only()` is left in the code. **Organizing Tests** explains why.

### Shared use Options

- Everything inside `use` applies to every test. Common options:

| Option | Example | Effect |
|---|---|---|
| `baseURL` | `'http://localhost:3000'` | `goto('/login')` becomes `goto('http://localhost:3000/login')` |
| `headless` | `false` | Shows the browser window |
| `viewport` | `{ width: 1280, height: 720 }` | Sets the page size |
| `ignoreHTTPSErrors` | `true` | Accepts test servers with self-signed certificates |
| `proxy` | `{ server: 'http://proxy:8080' }` | Sends traffic through a proxy |
| `launchOptions` | `{ slowMo: 500 }` | Passed to the browser launch, for example to slow every step down |
| `testIdAttribute` | `'data-qa'` | The attribute `getByTestId()` reads |

### Overriding Options with test.use()

- `test.use({ ... })` changes `use` options for one file or one `describe` group.

**Simple lesson:** `use` sets options for every test. `test.use()` changes them for a file or group.

## Projects

### What Projects Are

- A **project** is a named set of options. Playwright runs your tests once per project.
- Most configs have one project per browser. Others add phones, or a different `baseURL` for each environment.

### Multi-Browser Projects and Branded Browsers

- `devices['Desktop Chrome']`, `devices['Desktop Firefox']` and `devices['Desktop Safari']` give each project the right browser and settings.
- `channel: 'chrome'` or `channel: 'msedge'` uses the real branded Chrome or Edge installed on the machine.

```ts
projects: [
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
  { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  { name: 'Google Chrome', use: { ...devices['Desktop Chrome'], channel: 'chrome' } },
  { name: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
],
```

### Per-Project Options

- Each project has its own `use`, and can set `testMatch`, `retries`, `timeout` or `grep`.

- `default: false` keeps a project in the config without running it every time. `npx playwright test` skips it, `--project=firefox` runs it, and `--project="*"` runs every project.

### Project Dependencies and Setup Projects

- `dependencies: ['setup']` makes a project wait for the `setup` project to pass first.
- A **setup project** runs only setup files, for example a login that saves the session to a file.
- A **teardown** project (`teardown: 'cleanup'`) runs after everything that depends on the setup.
- If setup fails, the dependent projects do not run.

- **Authentication and Storage State** explains `storageState` in depth.

**Simple lesson:** projects run the same tests with different settings. Dependencies run setup first.

## Web Server

### webServer Option

- Most apps must be running before tests can visit them. `webServer` starts your app for you.

```ts
webServer: {
  command: 'npm run start',
  url: 'http://localhost:3000',
  reuseExistingServer: !process.env.CI,
  timeout: 120_000,
  env: { NODE_ENV: 'test' },
},
```

### Command, URL and Port

- `command` starts the app. `url` (or `port`) is where Playwright waits until the app answers.

### reuseExistingServer

- `true` reuses an app you already started yourself. On CI it should be `false`, so each run starts fresh.

### Server Timeouts and Environment

- `timeout` is how long to wait for the app to start (default 60 seconds). `env` passes environment variables to it.
- In the lab, the practice sites are always running. A `webServer` block just prints a note.

**Simple lesson:** `webServer` starts your app before the tests and stops it after.

## Timeouts

| Timeout | Default | Set it with |
|---|---|---|
| **Test timeout**: the whole test, hooks and fixtures | 30 seconds | `timeout: 60_000`, or `test.setTimeout()` |
| **Expect timeout**: each web-first assertion | 5 seconds | `expect: { timeout: 10_000 }` |
| **Action timeout**: each click, fill… | none (limited by the test) | `use: { actionTimeout: 10_000 }` |
| **Navigation timeout**: each `goto()` | none (limited by the test) | `use: { navigationTimeout: 30_000 }` |
| **Global timeout**: the whole run | none | `globalTimeout: 60 * 60 * 1000` |

- `test.setTimeout(120_000)` inside a test, or `test.slow()`, gives one test more time.
- The report page below takes 7 seconds to load. Run it, then raise `expect.timeout` in the config until it passes.

Raise one timeout for one slow check (`toHaveText(..., { timeout: 10_000 })`) rather than raising it for every test. A bigger global timeout makes every real failure slower to report.

**Simple lesson:** test 30 s, expect 5 s, actions and navigation share the test's time unless you set them.

## Parallelism

### Workers

- A **worker** is a separate process that runs tests. Several workers run tests at the same time.
- `workers: 4` uses four. The default is half the CPU cores. On CI, many teams use `1` or a fixed number.

### fullyParallel and How Tests Are Distributed

- By default, **files** run in parallel, and tests inside one file run one after another.
- `fullyParallel: true` lets tests inside a file run in parallel too.

<figure className="plw-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-plw-workers" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-workers">Animation: six tests from two files are shared among three workers. Each worker picks the next test when it finishes one</title>
  <g style={{ fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>
    <text x="20" y="54" style={{ fill: "var(--ink)" }}>Worker 1</text>
    <text x="20" y="104" style={{ fill: "var(--ink)" }}>Worker 2</text>
    <text x="20" y="154" style={{ fill: "var(--ink)" }}>Worker 3</text>
  </g>
  <g style={{ stroke: "var(--line)" }}><line x1="90" y1="62" x2="620" y2="62" /><line x1="90" y1="112" x2="620" y2="112" /><line x1="90" y1="162" x2="620" y2="162" /></g>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11 }}>
    <g><rect x="90" y="36" height="24" rx="5" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }}><animate attributeName="width" values="0;160;160" keyTimes="0;0.3;1" dur="8s" repeatCount="indefinite" /></rect><text x="100" y="53" style={{ fill: "var(--plw-blue)" }}>cart.spec › t1</text></g>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.3;0.31;1" dur="8s" repeatCount="indefinite" /><rect x="255" y="36" height="24" rx="5" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }}><animate attributeName="width" values="0;0;150;150" keyTimes="0;0.3;0.6;1" dur="8s" repeatCount="indefinite" /></rect><text x="265" y="53" style={{ fill: "var(--plw-blue)" }}>cart.spec › t3</text></g>
    <g><rect x="90" y="86" height="24" rx="5" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }}><animate attributeName="width" values="0;220;220" keyTimes="0;0.4;1" dur="8s" repeatCount="indefinite" /></rect><text x="100" y="103" style={{ fill: "var(--plw-blue)" }}>cart.spec › t2</text></g>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.4;0.41;1" dur="8s" repeatCount="indefinite" /><rect x="315" y="86" height="24" rx="5" style={{ fill: "var(--plw-green-soft)", stroke: "var(--plw-green)" }}><animate attributeName="width" values="0;0;170;170" keyTimes="0;0.4;0.75;1" dur="8s" repeatCount="indefinite" /></rect><text x="325" y="103" style={{ fill: "var(--plw-green)" }}>login.spec › t5</text></g>
    <g><rect x="90" y="136" height="24" rx="5" style={{ fill: "var(--plw-green-soft)", stroke: "var(--plw-green)" }}><animate attributeName="width" values="0;130;130" keyTimes="0;0.25;1" dur="8s" repeatCount="indefinite" /></rect><text x="100" y="153" style={{ fill: "var(--plw-green)" }}>login.spec › t4</text></g>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.25;0.26;1" dur="8s" repeatCount="indefinite" /><rect x="225" y="136" height="24" rx="5" style={{ fill: "var(--plw-green-soft)", stroke: "var(--plw-green)" }}><animate attributeName="width" values="0;0;140;140" keyTimes="0;0.25;0.5;1" dur="8s" repeatCount="indefinite" /></rect><text x="235" y="153" style={{ fill: "var(--plw-green)" }}>login.spec › t6</text></g>
  </g>
  <text x="320" y="190" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>With fullyParallel: true, any free worker takes the next test from any file.</text>
</svg>
<figcaption>Three workers share six tests. The run takes as long as the busiest worker.</figcaption>
</figure>

### Parallel and Serial describe Blocks

- `test.describe.configure({ mode: 'parallel' })` runs a file's tests in parallel.
- `test.describe.configure({ mode: 'serial' })` runs them in order. If one fails, the rest are skipped. Use it rarely: it makes tests depend on each other.

### Named Test Locks

- Some tests must never run at the same time, for example two tests that change the same shared account.
- Since Playwright 1.63, a test can take a **lock** name in its details: `test('update settings', { lock: 'shared-account' }, async ({ page }) => { ... })`. Tests that hold the same lock never run at the same time, even in different workers. `test.describe.configure({ lock: 'shared-account' })` puts a whole file or group under one lock.

### Shuffling and Repeating

- `--shuffle` runs test files in a random order. It prints a seed, so you can repeat that order. Hidden order dependencies show up fast.
- `--repeat-each=10` runs every test 10 times. Great for catching flaky tests before they reach CI.

The lab runs tests one at a time in your browser. Its first line prints the workers a real run would use, so you can see how your settings change it.

**Simple lesson:** workers run tests in parallel. `fullyParallel` splits files too. Keep tests independent so any order works.

## Retries

### Configuring retries and Flaky Tests

- `retries: 2` gives each failed test up to two more tries.
- A test that fails and then passes is reported as **flaky**. It still counts as a problem to fix.
- On CI, `retries: process.env.CI ? 2 : 0` is common. On your laptop, see failures at once.

### Retry Strategies and --fail-on-flaky-tests

- Retry on CI to keep the pipeline moving, and record a trace on the first retry (`trace: 'on-first-retry'`).
- `--fail-on-flaky-tests` makes flaky tests fail the run. Use it when your suite is stable and you want to keep it that way.

- Set `retries` to `0` and run again: the test fails. Then run with `--fail-on-flaky-tests` and `retries: 1`.

**Simple lesson:** retries rerun failed tests. Passing on a retry means flaky, not fixed.

## Environment Variables

### Using process.env

- `process.env.NAME` reads an environment variable. Configs use it to change behaviour on CI: `process.env.CI`.
- Never write passwords in test files. Read them from the environment.

### Loading .env Files with dotenv

- The `dotenv` package loads a `.env` file into `process.env`. Install it with `npm install -D dotenv`.
- Add `.env` to `.gitignore`, so secrets never reach your repository.

A `.env` file with real passwords must never be committed. On CI, set the same variables as secrets instead. **CI/CD and Scaling** shows how.

**Simple lesson:** read settings and secrets from the environment. `dotenv` loads them from a `.env` file.

## Command-Line Overrides

| Flag | Overrides | Example |
|---|---|---|
| `--workers` / `-j` | `workers` | `--workers=1` |
| `--retries` | `retries` | `--retries=2` |
| `--timeout` | `timeout` (test timeout) | `--timeout=60000` |
| `--max-failures` / `-x` | stop after N failures | `-x` stops at the first |
| `--list` | lists tests without running them | `--list --grep @smoke` |

- Try `--list`, `--retries=1`, and `--timeout=2000` too.

**Simple lesson:** command-line flags override the config for one run.

## Global Setup (Advanced)

### globalSetup and globalTeardown

- `globalSetup: './global-setup'` runs a file **once** before all tests. `globalTeardown` runs one after all tests.
- Use them for work that must happen once per run, such as seeding a database.

```ts
// global-setup.ts
import type { FullConfig } from '@playwright/test';

export default async function globalSetup(config: FullConfig) {
  console.log('Seeding test data for', config.projects.length, 'projects');
}
```

### Global Setup vs Setup Projects

- **Setup projects** are the recommended way. They appear in the report, get traces and use fixtures such as `page`.
- **globalSetup** is a plain function. It has no fixtures, no trace and no report entry.
- Use a setup project for logging in. Keep `globalSetup` for simple one-off jobs.

**Simple lesson:** prefer setup projects. Use `globalSetup` only for simple jobs with no browser.

## Try It in Your Project

- These steps take the lesson from the lab to your own app.
- Set `baseURL`, `trace: 'on-first-retry'` and CI-only retries in your config.
- Add a project for each browser your users need, and a mobile project if you have mobile users.
- Move secrets and URLs into environment variables and a `.env` file that git ignores.

**Simple lesson:** use one idea from this lesson on your own app this week.

## TL;DR

- `defineConfig()` in `playwright.config.ts` controls every run.
- `use` options apply to all tests. `test.use()` overrides them locally.
- Projects run the same tests in different browsers or setups. `dependencies` run setup first.
- `webServer` starts your app.
- Timeouts: test 30 s, expect 5 s. Workers and `fullyParallel` control parallel runs.
- `retries` reruns failures. Passing on a retry means flaky.
- Read secrets from `process.env`, loaded with `dotenv`. Flags override the config.

## Common mistakes

| Mistake | What happens | Fix |
|---|---|---|
| Full URLs in every `goto()` | Changing environments means editing every test | Set `baseURL` in `use` |
| Raising the global timeout to fix one slow test | Every failure takes longer to report | Use `test.slow()` or a per-assertion timeout |
| `retries: 3` everywhere and ignoring flaky tests | Real bugs hide behind retries | Retry on CI only, and fix flaky tests |
| Passwords written in the config | They leak into your repository | Use environment variables and `.env` |
| `mode: 'serial'` to share state | One failure skips many tests | Make tests independent. Use fixtures for shared setup |

## Check Yourself

## Exercises

## What to Remember

- The config sets defaults. Tests and flags override them.
- `baseURL`, projects and `webServer` are in almost every real config.
- Know your four timeouts: test, expect, action and navigation.
- Retries reveal flaky tests. Fix them.
- Secrets come from the environment.

Next: **Browser Contexts, Pages, and Emulation**
