skip to content

Why do teams prefer a Playwright setup project over globalSetup for suite preconditions?

level: seniorimportance: should knowfreq 46%

answer

  1. Two places to put a precondition
  2. One is inside the test model
  3. What the runner injects, and does not
  4. Evidence when the step breaks

basics

~20 s

Because a setup project is made of real tests, it gets fixtures, retries, a trace and a report entry, while globalSetup is a plain function the runner calls with none of those, so its failures leave only a log line.

solid answer

~50 s

`globalSetup` points at a module whose default export the runner calls once with the resolved config, outside the test model: no fixtures, no trace or video, no retries, and no entry in the report. A setup project is an ordinary `projects[]` entry other projects list in `dependencies`, so its bodies are real tests -- they get `page` and `request`, web-first assertions, the project's retry policy, and a trace you can open when they break. That difference decides most cases: preconditions that touch the internal admin console through a browser belong in a setup project. `globalSetup` still fits non-browser, once-per-run work such as starting a stub service, writing a config file or validating environment variables, and it can return a function that Playwright calls as teardown. `globalTeardown` covers cleanup with no owner in the setup function.

code

typescript · 11 lines
typescript
import { createServer } from 'node:http';
import type { FullConfig } from '@playwright/test';

export default async function globalSetup(config: FullConfig) {
  const stub = createServer((_req, res) => res.end('{"roles":["admin"]}'));
  await new Promise<void>(resolve => stub.listen(4500, resolve));
  console.log(`stub ready for ${config.projects.length} projects`);
  return async () => {
    await new Promise<void>(resolve => stub.close(() => resolve()));
  };
}

go deeper

for a junior

Know that both exist and that only one of them is made of tests. Recognise a globalSetup entry in the config as a path to a plain function the runner calls once.

for a middle

Explain what the runner injects into a setup project and withholds from globalSetup: fixtures, retries, artefacts and report entries, plus the config object globalSetup receives.

for a senior

Argue the choice from operability. Preconditions that need a browser or evidence on failure become setup projects; a stub process or an environment check stays in globalSetup.

for a principal

Set the house rule for where preconditions live so a suite does not accumulate two competing setup dialects, and decide what a precondition failure should look like to whoever is on call.

## Two different places to put "run this first" Playwright offers two mechanisms for work that must happen before the suite, and they are not equivalent. `globalSetup` is a path in `playwright.config.ts` pointing at a module whose default export is an async function; the runner calls it once, in its own process, before tests begin, passing the resolved configuration. A **setup project** is an ordinary entry in `projects[]` selected by its own `testMatch`, which other projects name in their `dependencies` array. The first is a function the runner happens to call. The second is a test the runner runs. ## What the runner gives each one | Capability | Setup project | `globalSetup` | |---|---|---| | Fixtures such as `page` and `request` | Yes, injected as in any test | No; you launch and close a browser yourself | | Trace, video and screenshots on failure | Yes, under the project's settings | No harness-level artefacts | | Retries | Yes, the project's retry policy applies | No; one attempt only | | Appears in the HTML report | Yes, as named tests | No, only as terminal output | | Runs per project, skippable with `--no-deps` | Yes | No; it runs for the whole invocation | | Receives the full resolved config object | Not directly | Yes, as its argument | ## Why the setup project usually wins - **Diagnosability.** When a precondition breaks in CI, a setup project failure is a named test with a trace you open in the viewer. A `globalSetup` failure is a stack trace in the log and nothing else. - **Consistency.** The same assertions, timeouts and fixtures apply, so the preparation code looks like the rest of the suite instead of a second dialect. - **Granularity.** Only the projects that list it in `dependencies` wait for it, and a `teardown` project can be attached to it. `globalSetup` is all-or-nothing for the whole run. - **Local ergonomics.** Running one project with `--no-deps` skips the preparation while you iterate; there is no comparable switch for `globalSetup`. ## What `globalSetup` is still good for 1. **Non-browser, once-per-invocation work** -- starting a stub for a service the admin console calls, generating a config file, validating that required environment variables are present before anything expensive begins. 2. **Work that must precede the whole project graph**, including setup projects themselves. 3. **Paired cleanup without a second project**: if `globalSetup` returns a function, Playwright calls that function after the run, which keeps the start and stop of a stub process in one file. 4. **Reading the resolved configuration**, since the function receives it and can branch on the projects or `use` values the run was actually launched with. There is also a matching `globalTeardown` path in the config for cleanup that has no natural owner in `globalSetup`. ## The usual mistake The failure mode is not choosing `globalSetup`; it is choosing it and then wanting test features from it. Code inside `globalSetup` that needs a browser has to call `chromium.launch()` and close it in a `finally` block by hand -- the runner injects nothing. There is no automatic retry, so an intermittent network call there fails the entire run rather than one test. And because it produces no artefacts, the only evidence of what went wrong is whatever you logged yourself. ## A working rule Ask what the step needs. If it needs a browser, assertions, or evidence when it fails, make it a setup project. If it is a process or a file that has to exist before the runner does anything else, and it fails loudly and deterministically, `globalSetup` is smaller and clearer. Suites that mix both are normal: a `globalSetup` starting a stub, and setup projects preparing the console. ## Migrating an existing globalSetup Moving a precondition into a setup project is usually mechanical: - Turn the function body into one or more `test()` calls in a file such as `console.setup.ts`. - Add a project selecting that file with `testMatch`, and list it in `dependencies` on the projects that need it. - Replace hand-rolled browser launch and close with the fixtures the runner injects. - Move any cleanup into a project named by the setup project's `teardown` property. - Keep in `globalSetup` only what genuinely has to precede the whole graph.

  • Can globalSetup use a browser at all, and what does that code have to do by hand?
    It can. You import a browser type such as `chromium`, call `chromium.launch()`, create a context and a page yourself, and close everything in a `finally` block. What you do not get is anything the runner would inject or record: no fixtures, no automatic cleanup, no trace, no retry. That manual burden is the argument for a setup project.
  • What does globalTeardown add if globalSetup can already return a teardown function?
    It is a separate config path for cleanup that has no natural owner in the setup function -- for example tearing down something a reporter or an external step created, or cleanup you want to keep in its own module. The returned closure is the neater choice when start and stop genuinely belong to the same file.

saying these in an interview costs you the question

  • Expects the page fixture to be available inside globalSetup
  • Thinks globalSetup failures produce a trace to open
  • Believes globalSetup is retried like a test
  • Says a setup project cannot use assertions or fixtures
  • Assumes globalSetup runs once per project or per worker