In Playwright, what does the await use(value) call do inside a test.extend fixture?
answer
- One function covers both phases
- There is a pause in the middle
- Above it setup, below it teardown
- The argument to use is injected
- Return provides nothing to the test
basics
~20 sIt 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 sA 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 linesimport { 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
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.
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.
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.
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