> ## Documentation Index
> Fetch the complete documentation index at: https://docs.e2e.army/llms.txt
> Use this file to discover all available pages before exploring further.

# test

> Tests, setup tests, groups, hooks, options, and fixtures.

`test` is the only registration surface. Every member registers synchronously
while the test file is imported.

```ts theme={"theme":"catppuccin-mocha"}
export interface TestAPI<Fixtures = TestFixtures> {
  (title: string, fn: TestFn<Fixtures>): TestCase;
  (title: string, options: TestOptions, fn: TestFn<Fixtures>): TestCase;
  skip(title: string, fn: TestFn<Fixtures>): TestCase;
  only(title: string, fn: TestFn<Fixtures>): TestCase;
  setup(title: string, options: SetupOptions, fn: SetupFn<Fixtures>): TestCase;
  extend<Extra>(): TestAPI<Fixtures & Extra>;
  extend<Extra>(fixtures: FixtureDefinitions<Fixtures, Extra>): TestAPI<Fixtures & Extra>;
  describe<Result>(title: string, body: SynchronousBody<Result>): void;
  describe<Result>(
    title: string,
    options: DescribeOptions,
    body: SynchronousBody<Result>,
  ): void;
  beforeEach(fn: TestHookFn): void;
  afterEach(fn: TestHookFn): void;
  beforeAll(fn: SuiteHookFn): void;
  afterAll(fn: SuiteHookFn): void;
}

export const test: TestAPI;
```

Calling any member outside collection, or after module evaluation finished,
throws `COLLECTION_ERROR`.

## Members

| Member          | Signature                                                            | Notes                                                                                                                                                                                        |
| --------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `test`          | `test(title: string, fn: TestFn): TestCase`                          | Returns an opaque handle. Discovery follows registration order; exporting the handle changes nothing.                                                                                        |
| `test`          | `test(title: string, options: TestOptions, fn: TestFn): TestCase`    | See [Options](#options).                                                                                                                                                                     |
| `test.skip`     | `skip(title: string, fn: TestFn): TestCase`                          | Body never runs. Reported as skipped, does not affect the exit code. For a reason, use `test(title, { skip: 'reason' }, fn)`.                                                                |
| `test.only`     | `only(title: string, fn: TestFn): TestCase`                          | Other ordinary tests are reported as `filtered`. In CI the run fails with `ONLY_IN_CI`, exit code 2.                                                                                         |
| `test.setup`    | `setup(title: string, options: SetupOptions, fn: SetupFn): TestCase` | `options.sessions` is required. Must be top-level, not inside `test.describe`.                                                                                                               |
| `test.describe` | `describe(title, body)` / `describe(title, options, body)`           | Group body must be synchronous. An `async` body is a type error and a `COLLECTION_ERROR`.                                                                                                    |
| `test.extend`   | `extend<Extra>()` / `extend<Extra>(fixtures)`                        | Without an argument, types an engine's contributed fixtures. With one, returns a new `test` that sets your own fixtures up around each attempt; see [Your own fixtures](#your-own-fixtures). |

```ts theme={"theme":"catppuccin-mocha"}
import { test } from '@e2edev/playwright';
import { expect } from '@e2edev/e2e';

test('app opens', async ({ app, web }) => {
  await app.open();
  await expect(web).toHaveURL('/');
});

test('checks out', { tags: ['billing'], retries: 2, timeout: 60_000 }, async ({ app }) => {
  await app.open('/checkout');
});

test.describe('checkout', { tags: ['smoke'], serial: true }, () => {
  test('adds an item', async ({ screen }) => {});
  test('pays', async ({ screen }) => {});
});
```

`test.setup` declares the sessions it saves, and must save each exactly once:

```ts theme={"theme":"catppuccin-mocha"}
test.setup('authenticate', { sessions: ['member'] }, async ({ app, screen, session }) => {
  await app.open('/login');
  await screen.getByLabel('Email').fill(credentials.user('member').username);
  await screen.getByLabel('Password').fill(credentials.user('member').password);
  await screen.getByRole('button', { name: 'Sign in' }).tap();
  await session.save('member');
});
```

Session names must match `/^[A-Za-z0-9_.-]{1,128}$/` and be unique.
Not saving a declared session, or saving one twice, throws `SESSION_CONTRACT`
at runtime.

```ts theme={"theme":"catppuccin-mocha"}
export interface SetupSession {
  save(name: string): Promise<void>;
}
```

## Hooks

| Member            | Signature                          | Fixtures        | Runs                                                                                                                                   |
| ----------------- | ---------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `test.beforeEach` | `beforeEach(fn: TestHookFn): void` | `TestFixtures`  | Before each test attempt, outermost group first, then in declaration order within a group                                              |
| `test.afterEach`  | `afterEach(fn: TestHookFn): void`  | `TestFixtures`  | After each test attempt, innermost group first, then in reverse declaration order within a group, including after a failure or timeout |
| `test.beforeAll`  | `beforeAll(fn: SuiteHookFn): void` | `SuiteFixtures` | Once per suite instance, on first entry into the scope                                                                                 |
| `test.afterAll`   | `afterAll(fn: SuiteHookFn): void`  | `SuiteFixtures` | Once per suite instance, when the scope's last test in the realm finishes                                                              |

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

```ts theme={"theme":"catppuccin-mocha"}
export interface TestOptions {
  timeout?: number;
  retries?: number;
  tags?: readonly string[];
  skip?: boolean | string;
  only?: boolean;
  platforms?: readonly Platform[];
  requires?: readonly Capability[];
  session?: string;
  agentContext?: string;
  agent?: string | readonly string[];
}

export interface DescribeOptions extends Omit<TestOptions, 'only'> {
  serial?: boolean;
}

export interface SetupOptions
  extends Omit<TestOptions, 'session' | 'only' | 'skip' | 'agent'> {
  sessions: readonly string[];
  agent?: string;
}
```

| Key            | Type                          | Default                  | Valid                                                            | Merge across layers                                                                                                                                                                                                                                                                                                                              |
| -------------- | ----------------------------- | ------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `timeout`      | `number`                      | `config.timeout`, 120000 | positive safe integer                                            | Innermost wins                                                                                                                                                                                                                                                                                                                                   |
| `retries`      | `number`                      | `config.retries`         | integer 0-10                                                     | Innermost wins; in a serial group the value comes from the group, not the member                                                                                                                                                                                                                                                                 |
| `tags`         | `readonly string[]`           | `[]`                     | -                                                                | Union of every layer, deduplicated                                                                                                                                                                                                                                                                                                               |
| `skip`         | `boolean \| string`           | unset                    | -                                                                | Any truthy layer skips; a string becomes the reason, otherwise `'skipped'`                                                                                                                                                                                                                                                                       |
| `only`         | `boolean`                     | unset                    | rejected in CI                                                   | Test level only, not on `DescribeOptions`                                                                                                                                                                                                                                                                                                        |
| `platforms`    | `readonly Platform[]`         | unset                    | -                                                                | Innermost wins, replace                                                                                                                                                                                                                                                                                                                          |
| `requires`     | `readonly Capability[]`       | `[]`                     | -                                                                | Innermost wins, replace                                                                                                                                                                                                                                                                                                                          |
| `session`      | `string`                      | unset                    | -                                                                | Innermost wins                                                                                                                                                                                                                                                                                                                                   |
| `agentContext` | `string`                      | unset                    | -                                                                | Concatenated outermost first, joined with a newline                                                                                                                                                                                                                                                                                              |
| `agent`        | `string \| readonly string[]` | the run's agents         | names in `agents`, each once; unknown is `COLLECTION_ERROR`      | Innermost wins, replace. Pins the test or group to configured agents; a list runs the test once per name, one result each. `--agent` narrows a pin to the names it also gives and never overrides a pin it does not name. A call's own `agent` option wins over it. Members of a serial group share the group's pin; a setup test pins one name. |
| `serial`       | `boolean`                     | `false`                  | groups only                                                      | Nesting inside a serial group is a `COLLECTION_ERROR`                                                                                                                                                                                                                                                                                            |
| `sessions`     | `readonly string[]`           | -                        | required on `test.setup`, static non-empty array of unique names | -                                                                                                                                                                                                                                                                                                                                                |

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

```ts theme={"theme":"catppuccin-mocha"}
export interface TestFixtures {
  readonly agent: Agent;
  readonly app: App;
  readonly screen: Screen;
  readonly platform: Platform;
}

export interface SetupFixtures extends TestFixtures {
  readonly session: SetupSession;
}

export interface SuiteFixtures {
  readonly platform: Platform;
}
```

| Callback                            | Fixture object  |
| ----------------------------------- | --------------- |
| `test`, `test.skip`, `test.only`    | `TestFixtures`  |
| `test.setup`                        | `SetupFixtures` |
| `test.beforeEach`, `test.afterEach` | `TestFixtures`  |
| `test.beforeAll`, `test.afterAll`   | `SuiteFixtures` |

| Fixture    | Type                          | Notes                                                                   |
| ---------- | ----------------------------- | ----------------------------------------------------------------------- |
| `agent`    | [`Agent`](/reference/agent)   | Acquiring it without a configured model fails with `MODEL_UNAVAILABLE`. |
| `app`      | [`App`](/reference/app)       | Always available.                                                       |
| `screen`   | [`Screen`](/reference/screen) | Always available.                                                       |
| `platform` | `Platform`                    | `'web' \| 'ios' \| 'android' \| (string & {})`.                         |

Everything else is a **contributed fixture** the target's engine declares, or
one of [your own](#your-own-fixtures). The zero-argument `test.extend<{ ... }>()`
types a contributed fixture without defining anything: `@e2edev/playwright`
contributes [`web`](/reference/playwright) 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.

```ts theme={"theme":"catppuccin-mocha"}
import { test as base } from '@e2edev/playwright';

export const test = base.extend<{ workspace: Workspace }>({
  workspace: async ({ web }, use) => {
    const workspace = await createWorkspace();
    await signIn(web, workspace);
    await use(workspace); // the hooks and the body run here
    await workspace.cleanup(); // teardown, after the body even when it failed
  },
});

test('renames the workspace', async ({ workspace, screen }) => {
  await screen.getByLabel('Name').fill(`${workspace.name} (renamed)`);
});
```

```ts theme={"theme":"catppuccin-mocha"}
export type FixtureFn<Fixtures, Value> = (
  fixtures: Fixtures,
  use: (value: Value) => Promise<void>,
) => Promise<void>;

export type FixtureDefinitions<Base, Extra> = {
  readonly [K in keyof Extra]: K extends keyof Base
    ? never
    : FixtureFn<Base, Extra[K]>;
};
```

`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:

1. 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 test `timeout` and a failure is reported in phase
   `beforeEach`, before any `beforeEach` hook has run.
2. `beforeEach` hooks, then the body, then `afterEach` hooks, all with the
   same fixture object. The fixture set is decided by the test's own chain:
   a hook registered through `base` that wraps a test registered through
   `test` sees `workspace` too, and a hook that reads `workspace` around a
   test registered through `base` fails with `UNSUPPORTED_CAPABILITY`.
3. `use` resolves. The continuation of each definition runs in reverse
   order, after the last `afterEach`, whether or not the body passed. Each
   teardown has its own `cleanupTimeout` budget like an `afterEach` hook; an
   error there fails a passing attempt in phase `afterEach` and joins
   `secondaryErrors` after a body failure.

What fails, and where:

| Mistake                                                                                                                                                                           | Fails                                             |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| A value that is not a plain object of functions, an empty name, a core name (`agent`, `app`, `screen`, `platform`, `session`), or a name an earlier `extend` in the chain defined | At import, `COLLECTION_ERROR` naming the fixture  |
| A name the target's engine contributes (`web` on a Playwright target)                                                                                                             | The attempt, in `beforeEach`, `TEST_SETUP_FAILED` |
| A definition that returns without calling `use`                                                                                                                                   | The attempt, in `beforeEach`, `TEST_SETUP_FAILED` |
| A definition that calls `use` twice                                                                                                                                               | The attempt, at teardown, `TEST_SETUP_FAILED`     |
| A definition that throws before `use`                                                                                                                                             | The attempt, in `beforeEach`, with that error     |
| A definition that throws after `use`                                                                                                                                              | The attempt, in `afterEach`, with that error      |

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.

## Function types

```ts theme={"theme":"catppuccin-mocha"}
export type TestFn<Fixtures = TestFixtures> = (fixtures: Fixtures) => void | Promise<void>;
export type SetupFn<Fixtures = TestFixtures> = (fixtures: Fixtures & SetupFixtures) => void | Promise<void>;
export type TestHookFn<Fixtures = TestFixtures> = (fixtures: Fixtures) => void | Promise<void>;
export type SuiteHookFn = (fixtures: SuiteFixtures) => void | Promise<void>;
export type FixtureFn<Fixtures, Value> = (
  fixtures: Fixtures,
  use: (value: Value) => Promise<void>,
) => Promise<void>;
export type SynchronousBody<Result> = Extract<
  Result,
  PromiseLike<unknown>
> extends never
  ? () => Result
  : never;

export interface TestCase {
  readonly [testCaseBrand]: true;
}
```

<CardGroup cols={2}>
  <Card title="Writing tests" href="/writing-tests" />

  <Card title="expect" href="/reference/expect" />

  <Card title="Signing in" href="/authentication" />

  <Card title="Errors" href="/reference/errors" />
</CardGroup>
