Skip to main content
The engine declares the app it drives. For a browser that is a URL and, optionally, the command that serves it. The runner starts the process, waits until the URL answers, and stops it when the run ends.

Point at a running app

Import playwright from @e2edev/playwright and put this in the exported config object:
Config fragment
app.open('/path') and every relative navigation resolve against url. The runner reads no APP_URL of its own; the scaffolded config reads it so you can override the address per run. A missing scheme becomes https://, or http:// for a loopback host.

Let the runner start it

Add command and the runner spawns the dev server, waits until url answers, and stops it when the run ends, fails, or is interrupted:
e2e.config.ts
Three options matter most:
  • reuseExisting: true attaches to a dev server you already have running instead of failing with APP_ALREADY_RUNNING. CI ignores it, so a leftover server on a shared runner is still an error there.
  • log keeps what the server printed. Without it the runner discards the output, and a dev server that crashes on boot leaves nothing behind but APP_UNREACHABLE. The file is unredacted; keep it in an ignored directory. With log set, a startup failure (an exit before the URL answered, or startupTimeout spent) ends its APP_UNREACHABLE message with the last 20 lines appended to the log since the command started, each command.env value replaced by its <secret:NAME> marker, so the report alone shows what the process was doing. A wait that passes half of startupTimeout prints one notice, target "web" command still starting after 30s: waiting for http://localhost:3000/; log: .e2e/logs/app.log, once that half is at least 5 s.
  • env passes what the app needs. The child inherits only PATH, HOME, the temp-directory variables, and on Windows SystemRoot and COMSPEC. Model keys and CI tokens are not inherited automatically. Values you put in command.env are forwarded to the child, including any keys or tokens.
The command is never shell-interpreted: executable is resolved on PATH and args are passed verbatim. startupTimeout (default 60 s) bounds the wait for url; set readyUrl when the health endpoint is not the base URL. Every option is in the config reference.

Start dependencies first

A database, a migration, an auth emulator: declare them as services. They start in order before the app command, each ready before the next begins, and stop in reverse when the run ends.
Target fragment
A service is ready when its readyUrl answers, or, with waitForExit, when the process exits 0. Each one takes the same log, env, and cwd options as command. The list reporter shows each service starting with a ticking clock and splits the startup time out of the run’s duration. A service still not ready at half its startupTimeout prints one notice (service "postgres" still starting after 90s: waiting for it to exit), provided that half is at least 5 s; a service that exits non-zero or runs out of budget ends its APP_UNREACHABLE with the lines appended to its log since it started, so a docker compose up --wait that hangs on Image postgres Pulling says so in the report. Services that share one log file also share that tail: an earlier readyUrl service still running can add lines to it.

Several targets

Two browsers on one app are two targets that each name it:
e2e.config.ts
Targets that declare the same command share one process. Every test runs on every target unless it is scoped with platforms or requires; --target <name> selects at run time. A device target sits beside a browser target in the same list; see the agent-device reference.

Protected previews

The app under test is often not public: a Vercel preview behind deployment protection, a tunnel with an interstitial page, a staging host behind HTTP basic authentication. The engine declares how to get through, and every path onto the page goes through with it.
Target fragment
headers go on every request the browser sends to an allowed origin and on no request to any other, so a bypass secret never reaches a CDN or an identity provider the page also talks to. The same option skips ngrok’s interstitial with 'ngrok-skip-browser-warning': '1'. A preview origin changes with every deploy. identity keeps cache and session entries keyed by what the app is, so they survive a redeploy. A host behind HTTP basic authentication takes basicAuth. The browser answers a 401 from an allowed origin with the credentials and a challenge from any other origin with nothing.
Target fragment
What headers costsInjecting headers routes every request through the runner, which turns the browser’s HTTP cache off and blocks service workers for that target. A Playwright trace records request headers, so the trace of a protected preview carries the bypass secret. Share it as you would the secret.

Config reference

command, services, and every app option.

Playwright reference

Browser, viewport, connect, headers, basicAuth.