skip to content

Extending the Base

Writing your own fixture with test.extend, where the await use(value) call splits setup from teardown, and overriding a built-in by name. Asked because it replaces before and after hooks.

on this pageshow

explore

questions

6

In Playwright, what does the await use(value) call do inside a test.extend fixture?

level: juniorimportance: must knowfreq 74%

answer

  1. One function covers both phases
  2. There is a pause in the middle
  3. Above it setup, below it teardown
  4. The argument to use is injected
  5. Return provides nothing to the test

basics

~20 s

It hands the fixture's value to the test and pauses the fixture there. Everything written above the call is setup; everything below it is teardown, which runs once the test finishes, whether it passed or failed.

solid answer

~50 s

A fixture registered through `test.extend` is a single async function with the signature `async ({ deps }, use) => {}`. You build whatever the test needs, then call `await use(value)`. Playwright suspends the fixture at that line, injects `value` under the fixture's name into every test that lists it, and resumes the function once the test body has finished. So the one `use` call is the seam: the lines above it do the job of `beforeEach`, the lines below it do the job of `afterEach`, and the two halves live in the same function instead of two hooks that must be kept in sync. The promise from `use` resolves after the test ends whether it passed or failed, so cleanup written after it still runs on a failing test. Returning a value with `return` instead of passing it to `use` provides nothing and Playwright reports it as an error.

code

typescript · 21 lines
typescript
import { test as base, expect } from '@playwright/test';

type StaffFixtures = { auditorSeat: { id: string; email: string } };

export const test = base.extend<StaffFixtures>({
  auditorSeat: async ({ request }, use) => {
    const created = await request.post('/admin/api/staff', {
      data: { role: 'auditor', email: '[email protected]' },
    });
    const seat = await created.json();

    await use(seat);

    await request.delete(`/admin/api/staff/${seat.id}`);
  },
});

test('an auditor sees the read-only banner', async ({ page, auditorSeat }) => {
  await page.goto(`/admin/staff/${auditorSeat.id}`);
  await expect(page.getByRole('status')).toHaveText('Read-only access');
});

go deeper

for a junior

Remember the three-part shape: setup, await use(value), teardown. If you can write a fixture that opens something, hands it over and closes it again, you have the whole idea.

for a middle

Be ready to say that use suspends the fixture rather than returning from it, and that the promise it hands back resolves after the test body ends, which is exactly why the lines below it are the teardown.

for a senior

Show that cleanup after use still runs on a failing test, while setup that throws earlier leaves nothing cleaned up. Order the setup so the fragile step happens as late as possible.

for a principal

Frame the use seam as the suite's ownership contract: every created resource has one function that also destroys it, so nobody has to audit a pile of afterEach hooks looking for leaks.

## The shape of a custom fixture `test.extend` takes an object whose keys are fixture names and whose values are async functions. Each function has the signature `async ({ ...dependencies }, use, testInfo) => { ... }`. The first argument destructures the other fixtures this one needs, the second is the `use` callback Playwright passes in, and the optional third is the `TestInfo` object for the test currently running. The important mental correction: **a fixture function is not a factory that returns a value**. It is a function that wraps itself around the test. It runs, stops in the middle, lets the test run, then continues. ## What await use(value) actually does 1. Playwright resolves the fixture's dependencies, then calls the fixture function. 2. The function runs its setup lines and reaches `await use(value)`. 3. Playwright suspends the function there and injects `value` into every test, hook and dependent fixture that names this fixture. 4. When the test body finishes — passing, failing, or timing out — Playwright resumes the function, and the promise returned by `use` resolves. 5. The remaining lines run as teardown, and the fixture is finished. ## The seam maps onto the hooks it replaces | Hook style | Fixture style | |---|---| | the body of `test.beforeEach` | the lines above `await use(value)` | | a module-level variable holding the value | the argument passed to `use` | | the body of `test.afterEach` | the lines after `await use(value)` | The fixture version wins on three counts: - **Setup and its matching cleanup sit in one function**, so a reviewer sees both or neither. Deleting the setup deletes the cleanup. - **The value is named and typed.** With hooks you smuggle the created object out through a variable in module scope; with a fixture it arrives in the test's argument list. - **A test's dependencies are visible in its signature.** Reading `async ({ auditorSeat }) => {}` tells you what this test starts from without opening another file. ## Cleanup runs on failing tests too A common assumption is that a failing test skips the fixture's teardown. It does not. `await use(value)` resolves once the test body has finished, and the runner then continues the fixture regardless of the verdict. That is why the documented pattern has no `try/finally` around `use` — for the ordinary failure path it is not needed. What *does* skip the teardown is a fixture that throws **before** it reaches `use`. Execution never gets to the bottom half, so anything already created above the throw is not removed by that fixture. Keep the risky part of setup as close to `use` as possible, or wrap it yourself. ## The classic mistake: return instead of use ```typescript // Wrong: the value is never provided, and there is nowhere to put teardown. const test = base.extend({ seat: async ({ request }) => { return await createSeat(request); }, }); // Right: the value is provided, and the line after use is the teardown. const test = base.extend({ seat: async ({ request }, use) => { const seat = await createSeat(request); await use(seat); await deleteSeat(request, seat.id); }, }); ``` `use` is called exactly once per fixture instance. If a test needs several of something — three staff seats in an internal admin console, say — provide a **factory function** through `use`, let the test call it as often as it likes, and have the fixture record what was created so the lines after `use` can remove all of it. ## A worked shape for an admin console For a console with several staff roles, a typical fixture creates a seat over the API, hands the seat object to the test, and deletes it afterwards. Every test that names `auditorSeat` gets its own freshly created seat and its own deletion; tests that do not name it are untouched by any of it. ## Worth remembering - One fixture function, one `use` call, two phases. - The value the test sees is the argument to `use`, never a `return` value. - Teardown after `use` survives a failing test; it does not survive a setup that threw earlier. - The third parameter, `testInfo`, is there when you need the running test's metadata inside the fixture.

  • What happens if a fixture body throws before it reaches await use(value)?
    The fixture fails, and every test depending on it is reported failed without running its body. The lines below `use` never execute, so whatever was created above the throw is not cleaned up by that fixture. Do the fragile part of setup last, or guard it yourself.
  • Can a fixture call use more than once?
    No. A fixture provides one value per scope and calls `use` once. When a test needs several instances, pass a factory through `use`, let the test call it repeatedly, and keep a list inside the fixture so the code after `use` can remove everything the test created.

It works like a lending desk: the fixture prepares the item, slides it across the counter for as long as the test needs it, and files it away again the moment the test is done.

saying these in an interview costs you the question

  • Says a fixture returns its value with return instead of use
  • Thinks the code after use runs before the test body
  • Believes cleanup after use is skipped when the test fails
  • Assumes every fixture needs try/finally to survive a failure
  • Puts teardown in afterEach while the value lives in a fixture
open as a page

In Playwright, how do you override the built-in page fixture without losing the original?

level: middleimportance: must knowfreq 57%

basics

~20 s

Define a fixture with the same name in test.extend and destructure that same name in its first argument. Playwright passes the original value in, so you can configure it and then hand the very same object to use.

open as a page

In Playwright, in what order do dependent custom fixtures set up and tear down around a test?

level: middleimportance: should knowfreq 46%

basics

~20 s

A fixture asks for another by naming it in its first argument, and Playwright builds a dependency graph from those names. Dependencies set up first and tear down last: teardown runs in the reverse order of setup.

open as a page

In Playwright, a fixture seeds a staff account and deletes it after await use() — when does that delete never run?

level: seniorimportance: should knowfreq 36%

basics

~20 s

When the fixture throws before it reaches use, because execution never gets to the lines below. Once use has been reached, Playwright resumes the fixture after the test, so a failing or timed-out test still triggers the delete.

open as a page

In Playwright, how do you decide what becomes a custom fixture and what stays in test.beforeEach?

level: principalimportance: should knowfreq 40%

basics

~20 s

Setup that produces something the test uses belongs in a fixture: it names what it provides, pairs its own teardown, and runs only for tests that ask for it. Local arrangement for one file can stay in a hook.

open as a page

In Playwright, how do you use two separately extended test objects in one spec file?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

Combine them with mergeTests from the Playwright test package: mergeTests(staffTest, auditTest) returns one test object carrying both fixture sets. Importing both objects side by side does not work, and mergeExpects does the same job for custom matchers.

open as a page