skip to content

Preconditions and Teardown

Making something run before the suite and something else after it: a project that depends on a setup project, the older global hooks, and the option that starts the app and waits for its URL.

on this pageshow

explore

questions

5

What does Playwright's webServer config option do before the first test runs?

level: juniorimportance: must knowfreq 71%

answer

  1. Config key that boots the app
  2. Waiting before the first test
  3. Two ways to prove readiness
  4. Different answer locally and in CI

basics

~10 s

Playwright's webServer option spawns a command before the suite and waits until the url or port it names answers, then runs the tests and stops the process it started when the run ends.

solid answer

~40 s

`webServer` in `playwright.config.ts` names a `command` Playwright spawns before any test runs, plus either a `url` it polls until the response status is 2xx, 3xx, 400, 401, 402 or 403, or a `port` it waits to accept connections. Only when that readiness check passes does the runner start tests, so the internal admin console is guaranteed to be up. `reuseExistingServer: !process.env.CI` is the usual setting: locally it attaches to a dev server you already have running, while in CI it insists on starting its own. `cwd`, `env`, `stdout`, `stderr` and `gracefulShutdown` shape the child process, and `timeout` bounds the wait -- exceeding it fails the run before any test executes. Playwright kills the process it started at the end, and since 1.43 the key also accepts an array when a suite needs several services.

code

typescript · 12 lines
typescript
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: { baseURL: 'http://localhost:4173' },
  webServer: {
    command: 'pnpm run start:admin-console',
    url: 'http://localhost:4173/health',
    timeout: 120_000,
    reuseExistingServer: !process.env.CI,
    stdout: 'pipe',
  },
});

go deeper

for a junior

Know that this config key starts the app for you and waits for it, and be able to point at the command plus url or port pair in a config you are shown.

for a middle

Explain the readiness check itself: which statuses count as up, how port differs from url, what the timeout bounds, and why the process is killed when the run finishes.

for a senior

Show the CI judgment: reuse locally but never in CI, pipe server logs so a startup failure is diagnosable, and know that each shard starts its own server.

for a principal

Own the choice of whether the suite starts the application at all, versus testing a deployed environment, and be clear about what each option costs in reproducibility.

## What the option actually does Playwright's `webServer` block in `playwright.config.ts` hands the lifecycle of the application under test to the test runner. You give it a `command` -- the same shell line you would type by hand to start the internal admin console -- and Playwright spawns that command as a child process before it starts executing tests. It then blocks, polling for a readiness signal, and only once that signal arrives does the first test file run. When the run ends, Playwright terminates the process it started. No test code has to know that the server was started for it; the suite simply finds the app already listening. ## Two ways to say "ready" Exactly one of `url` or `port` is required, and the choice decides how Playwright probes for readiness. | Option | Readiness check | When to reach for it | |---|---|---| | `url` | Requests that URL until the response status is 2xx, 3xx, 400, 401, 402 or 403 | An HTTP app, where a health route -- or even a redirect to sign-in -- proves the process is answering | | `port` | Waits until that port on `localhost` accepts a TCP connection | A service with no convenient HTTP route to poll | The accepted status list matters in practice. A `401` from the admin console's protected root still means "the server is answering", so you do not have to add an unauthenticated health endpoint just to satisfy the runner. A `500`, by contrast, does **not** count as ready: Playwright keeps waiting until `webServer.timeout` expires and then fails the whole run before a single test has executed. ## The rest of the options - `command` -- required; the shell command Playwright spawns. - `cwd` -- the working directory for that command; resolved relative to the config file by default. - `env` -- extra environment variables handed to the child process only. - `timeout` -- how long to wait for the readiness signal before failing the run. - `reuseExistingServer` -- when `true` and something already answers at the `url` or `port`, Playwright uses it instead of spawning a second one. - `stdout` and `stderr` -- `'pipe'` to surface the server's logs in the run output, `'ignore'` to silence them. - `ignoreHTTPSErrors` -- for a dev server presenting a self-signed certificate. - `gracefulShutdown` -- for example `{ signal: 'SIGTERM', timeout: 500 }`, to ask the process to stop before it is killed. ## Local versus CI 1. On a developer machine the admin console is usually already running under a dev server, and restarting it for every run would be slow and would throw away hot reload. 2. On CI nothing is running, and silently attaching to a stray process would make the run test something other than the commit under build. 3. `reuseExistingServer: !process.env.CI` encodes both rules in one line: attach locally, insist on a fresh process in CI, and fail loudly there if the port is already taken. ## Where it sits in the run order - Web server processes are started before `globalSetup` runs and before any project's tests, including a setup project, so a precondition step can already talk to the app. - Since Playwright 1.43 the key also accepts an **array** of server definitions, which is how a suite boots the admin console and a stub for a downstream service together. - Every invocation of `playwright test` owns its own server, so a sharded CI matrix starts one server per shard rather than sharing a single instance. ## What it is not - It is not a per-test hook. One process serves the entire run and every worker in it, so it cannot reset application state between tests. - It is not a health model for production; the readiness probe only proves the process answers, not that background migrations or caches have finished. - It is not required. Teams that deploy the console to a shared environment before the suite runs simply omit `webServer` and point `use.baseURL` at that environment instead. ## Diagnosing a start-up failure When the run dies before any test, the message names the readiness check that never passed, and the useful next steps are mechanical: 1. Run the `command` by hand in the same directory and watch what it prints. 2. Set `stdout: 'pipe'` so the server's own log reaches the run output instead of being swallowed. 3. Confirm the probe target: a `url` on the wrong path answers `404`, which is not in the accepted list. 4. Raise `timeout` only after establishing that the server really is slow to boot rather than broken.

  • The admin console's root returns 401 until you sign in. Can it still be used as the webServer url?
    Yes. Playwright treats 2xx, 3xx, 400, 401, 402 and 403 as proof the server is answering, so a protected root is a perfectly good readiness probe. Only a connection refusal or a 5xx keeps it waiting, so you do not need to add an unauthenticated health route just to satisfy the runner.
  • Why would reuseExistingServer be wrong to leave true on CI?
    On CI it would let the run silently attach to whatever already occupies the port -- a leftover process from an earlier job, or a different build. The suite would then test something other than the commit under build and pass for the wrong reason. Setting it to false makes a busy port a loud failure instead.

saying these in an interview costs you the question

  • Thinks tests start immediately and race the server's startup
  • Says reuseExistingServer should stay true in CI
  • Believes the server must be started by hand in globalSetup
  • Assumes Playwright leaves the spawned server running afterwards
  • Thinks a url answering 500 counts as ready
open as a page

In a Playwright config, what does naming a project in another project's dependencies guarantee?

level: middleimportance: must knowfreq 62%

basics

~10 s

It guarantees ordering: every test in the named project finishes before the dependent project starts, and if any of them fails, the dependent project's tests are skipped rather than run.

open as a page

Why does a Playwright run with --project=admin-console --no-deps fail when the same command without the flag passes?

level: middleimportance: should knowfreq 34%

basics

~20 s

Because --no-deps tells Playwright to run only the named project and skip the projects it lists in dependencies, so whatever the setup project would have prepared is missing or stale and the tests break on the precondition.

open as a page

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

level: seniorimportance: should knowfreq 46%

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.

open as a page

Your Playwright suite spends six minutes in setup projects before any test starts -- how do you decide what stays a suite-level precondition?

level: principalimportance: should knowfreq 27%

basics

~20 s

Judge each step by who needs it, how often it must run, and whether it needs a browser. Split one broad setup project into narrow ones so a project waits only for what it lists.

open as a page