test is the only registration surface. Every member registers synchronously
while the test file is imported.
COLLECTION_ERROR.
Members
test.setup declares the sessions it saves, and must save each exactly once:
/^[A-Za-z0-9_.-]{1,128}$/ and be unique.
Not saving a declared session, or saving one twice, throws SESSION_CONTRACT
at runtime.
Hooks
Hook order follows scope nesting, not position in the file: a file-level
beforeEach declared below a test.describe still runs before the group’s
own beforeEach, and a hook declared after a test still applies to it.
A suite instance is one execution realm: the module is re-imported for every
retry, for every serial group, and for every setup test, so beforeAll runs
again in each. A scope’s afterAll runs as soon as its last runnable test in
that realm is done, so one group’s teardown never runs after a sibling group’s
tests. A realm that a failure discards runs afterAll for every scope whose
beforeAll started. A failing afterAll ends the realm too: later tests in the
file start in a fresh one, and inside a serial group the remaining members are
skipped with cause hook-failed and the group fails without a retry.
beforeEach and the body share the test timeout. Each afterEach hook then
gets its own cleanupTimeout budget with working fixtures, so teardown can
still drive the app after the body timed out. A hook that overruns that budget
fails, its fixture operations are cancelled, and the next hook still runs.
A throw inside beforeAll or afterAll is reported as HOOK_FAILED and skips
the suite’s tests with cause hook-failed. Inside a serial group, a
beforeAll failure skips every remaining member and fails the group without
retrying it.
Options
Resolution order: test, nearest group, outer groups, config, built-in default.
serial: true makes the group one ordered retry unit with shared app state.
Secret redaction and pixel taint share that lifetime: provider-resolved values
stay redacted in later members, and a secret fill withholds model pixels for
the rest of the group’s attempt. A retry starts fresh isolation and fresh
secret state.
Inside it, retries, session, platforms, requires, skip, only, and a
nested serial on a member are collection errors. Set them on the group.
Fixtures
Everything else is a contributed fixture the target’s engine declares, or
one of your own. The zero-argument
test.extend<{ ... }>()
types a contributed fixture without defining anything: @e2edev/playwright
contributes web and exports a test already typed
with it; a device engine contributes device the same way. Reaching for a
fixture neither the engine nor a test.extend() defines fails at first touch
with UNSUPPORTED_CAPABILITY; declare requires: ['web'] to skip such tests
at selection instead.
Fixtures are lazy. Destructuring in the callback parameter list acquires them
before the first statement of the body.
Your own fixtures
test.extend(fixtures) defines a fixture per test: a resource the body needs
set up before it runs and cleaned up after, however the body ended. Each
definition is one function in Playwright’s shape. Everything before
await use(value) is the setup, value is what the test receives, and
everything after is the teardown.
extend returns a new test; the one it was called on is unchanged. The
new object carries every definition of the chain, the parent’s first, and
every test, setup test, or hook registered through it records that chain.
A definition is typed against the fixtures of the test it extends, not its
siblings in the same call. A fixture that needs another fixture goes in a
chained second extend: base.extend({ a }).extend({ b }) gives b a
fixture object that already has a.
Timing, per attempt:
- The engine’s fixtures exist. Each definition runs in declaration order,
with the fixtures defined so far, until it calls
use. Setup counts against the testtimeoutand a failure is reported in phasebeforeEach, before anybeforeEachhook has run. beforeEachhooks, then the body, thenafterEachhooks, all with the same fixture object. The fixture set is decided by the test’s own chain: a hook registered throughbasethat wraps a test registered throughtestseesworkspacetoo, and a hook that readsworkspacearound a test registered throughbasefails withUNSUPPORTED_CAPABILITY.useresolves. The continuation of each definition runs in reverse order, after the lastafterEach, whether or not the body passed. Each teardown has its owncleanupTimeoutbudget like anafterEachhook; an error there fails a passing attempt in phaseafterEachand joinssecondaryErrorsafter a body failure.
A body or hook that hits the test
timeout is abandoned, not cancelled:
teardown starts while it may still be running, as with afterEach. A
fixture whose setup was abandoned the same way has no teardown to run yet;
if it reaches use later, use resolves at once (there is no attempt to
hand the value to) and the code after it runs detached from the run, so what
the setup allocated is still released.
