skip to content

Project Matrix

The projects array that runs the same tests under different options - one project per browser is the canonical shape - plus selecting one on the command line and overriding options inside a file.

on this pageshow

explore

questions

5

In a Playwright config, what does the `projects` array do?

level: juniorimportance: must knowfreq 78%

answer

  1. Named run variants inside the config
  2. Same spec, one result per entry
  3. Each entry carries its own use block
  4. Presets spread into use
  5. --project selects one by name

basics

~20 s

The projects array lists named run variants, each with its own use options and file filters. Playwright runs every matching spec once per project, so one test yields one result per project, and --project=<name> runs a single variant.

solid answer

~40 s

`projects` is an array of named run configurations in `playwright.config.ts`. Each entry needs a `name`, and typically a `use` block supplying the options its tests get — the canonical shape spreads a preset into it, `use: { ...devices['Desktop Chrome'] }`. A project may also narrow its own file set with `testDir`, `testMatch` and `testIgnore`, and set its own `retries`. Playwright resolves the file list once per project and runs every matching spec in each, so a suite of 100 tests across three projects is 300 results, each tagged with the project name in the report. `--project=admin-webkit` runs one variant, and the flag can be repeated. With no `projects` array at all there is a single unnamed run and nothing to select.

code

typescript · 11 lines
typescript
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  use: { headless: true },
  projects: [
    { name: 'admin-chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'admin-firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'admin-webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

go deeper

for a junior

Recall that projects is an array of named run variants, that each usually spreads a device preset into use, and that --project=<name> runs just one of them.

for a middle

Explain the multiplication: the file list is resolved per project, so every matching spec runs once per project and produces its own result and retry budget.

for a senior

Show judgment about matrix cost in CI — each added project re-runs everything it matches, so know which per-project fields let you narrow a variant instead of duplicating the whole suite.

for a principal

Own the question of which axes deserve to be projects at all, given that the cost is multiplicative and every axis adds a report dimension the team has to triage.

## What a project actually is A **project** in Playwright is a *named run variant*, not a folder or a workspace. The `projects` array in `playwright.config.ts` holds one object per variant, and the only field a project truly needs is `name` — the label the reporter prints and the value `--project` accepts. Everything else on the entry reshapes that variant of the run: - `use` — the option values the tests in this project receive (the canonical entry spreads a preset: `use: { ...devices['Desktop Chrome'] }`). - `testDir` — the root this project scans for spec files, overriding the top-level `testDir`. - `testMatch` / `testIgnore` — which of the files under that root this project runs and skips. - `retries` — how many times this project's failures are re-attempted, overriding the top-level value. The mental model that keeps this straight: the file set and the option set are both properties of the *project*, not of the suite. Two projects can run completely different files, identical files with different options, or anything between. ## What happens at run time 1. Playwright reads the config and builds the list of projects, filtered by any `--project` flags. 2. For each surviving project, it resolves that project's file list from its `testDir`, `testMatch` and `testIgnore`. 3. Each test found in a project becomes its own test instance, with its own options, result, retry budget and report row. The consequence that surprises people: **projects multiply the suite, they do not divide it.** One spec file listed in three projects produces three results for every test in it. A hundred tests across three projects is three hundred runs, and adding a fourth project adds another hundred. Inside a test, `test.info().project.name` tells you which variant you are executing in. ## The canonical shape: one project per browser ```ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', projects: [ { name: 'admin-chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'admin-firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'admin-webkit', use: { ...devices['Desktop Safari'] } }, ], }); ``` `devices` is exported from `@playwright/test` alongside `defineConfig`; each preset is a plain object of option values, so spreading it into `use` and then adding your own keys after the spread lets you override individual entries of the preset. Browser coverage is the most common axis, but nothing about `projects` is browser-specific. A staff console suite might just as reasonably carry a `smoke` project that runs a handful of specs on every push and a `regression` project that runs the rest nightly — same mechanism, different axis. ## Selecting projects on the command line - `npx playwright test --project=admin-webkit` runs exactly that project. - The flag is repeatable: `--project=admin-chromium --project=admin-webkit` runs two. - A name that matches nothing aborts the run with an error naming the configured projects — it does not silently run zero tests. - `npx playwright test --list` prints the tests that would run, each prefixed with its project name, which is the quickest way to confirm a matrix does what you think. - A project with no `name` cannot be selected, so name every entry. ## Which fields belong on a project | Field | Controls | Typical value | |---|---|---| | `name` | Report label and `--project` selector | `'admin-webkit'` | | `use` | Options handed to this project's tests | `{ ...devices['Desktop Safari'] }` | | `testDir` | Root scanned for this project's specs | `'./tests/admin'` | | `testMatch` | Files this project runs | `/.*\.smoke\.spec\.ts/` | | `testIgnore` | Files this project skips | `'**/bulk-import.spec.ts'` | | `retries` | Retry budget for this project only | `2` | Anything a project does not set falls back to the top-level value of the same key, merged key by key, so a shared `use` block at the config root plus a small per-project `use` is the normal layout. ## Where teams go wrong - Treating projects as a way to *split* work — they repeat it; splitting a run across machines is a different mechanism entirely. - Leaving a project unnamed, then wondering why `--project` cannot address it. - Assuming a top-level `use` value wins over a project's: the project's value is the more specific one and takes precedence. - Forgetting that the matrix is multiplicative when estimating CI wall-clock: each new entry re-runs everything it matches.

  • How do you run two of the configured projects in one command, and what happens if you pass a name that does not exist?
    Repeat the flag: `--project=admin-chromium --project=admin-webkit`. Both projects run in the same invocation and the report keeps them apart by name. A name that matches no project is an error — Playwright aborts and prints the configured project names rather than running an empty suite.
  • If a config has no `projects` array at all, what does Playwright run?
    A single implicit, unnamed run using the top-level `testDir`, `use` and `retries`. Every spec runs exactly once, the reporter shows no project label, and `--project` has nothing to select. Adding the array is what turns one pass over the suite into a matrix.

One recipe cooked in three different ovens: the recipe never changes, the oven settings do, and you get three cakes to compare rather than one cake baked faster.

saying these in an interview costs you the question

  • Thinks projects split the suite instead of repeating it
  • Believes a project can only ever mean a browser
  • Expects an unnamed project to be selectable on the command line
  • Thinks --project takes a file path or a glob of specs
  • Assumes the top-level use block wins over a project's use
open as a page

In Playwright, how does `test.use()` in a spec file interact with a project's `use` options?

level: middleimportance: must knowfreq 62%

basics

~20 s

test.use() merges over the project's use block key by key for the tests in its scope, so the file wins on the keys it names and inherits the rest. It applies in every project that runs the file.

open as a page

In a Playwright project, how do `testDir`, `testMatch` and `testIgnore` decide which specs run?

level: middleimportance: should knowfreq 47%

basics

~20 s

Each project resolves its own file list: testDir is the root it scans, testMatch keeps files matching a glob or regex, and testIgnore drops files. Ignore beats match, and each key overrides the top-level value for that project only.

open as a page

One project in a Playwright matrix is intermittently red while the others are green — which project-level options contain it without editing the specs?

level: seniorimportance: should knowfreq 41%

basics

~20 s

Reproduce with --project on the failing variant, then contain it in the config: raise retries on that project only, adjust its use options, and drop the genuinely unsupported specs with that project's testIgnore. Leave the other projects untouched.

open as a page

When should a new variation in a Playwright suite become another entry in `projects` rather than a `test.use()` in the spec files?

level: principalimportance: should knowfreq 31%

basics

~20 s

Make it a project when the variation is an axis the whole suite should run under and be reported on separately. Keep it in test.use when only certain specs need it, since a project re-runs everything.

open as a page