JS Sketchbooksee JavaScript think ✏️
← back to phase 11

11.7 — Fixtures & hooks

Since 11.1 you’ve typed async ({ page }) without asking the obvious question: you never created page — who hands it in? Fixtures: prepared resources the runner builds and injects by name — each test receiving a genuinely FRESH browser world. 10.6’s dependency injection, promoted to infrastructure.

watch it happen
import { test, expect } from "@playwright/test";

test.beforeEach(async ({ page }) => {
  await page.goto("/");
});

test("search filters the list", async ({ page }) => {
  // page: fresh browser context, navigated by the hook
});

// a CUSTOM fixture: a logged-in page
const testWithShopper = test.extend({
  shopper: async ({ page }, use) => {
    await page.goto("/login");            // setup
    await page.getByLabel("Email").fill("t@shop.com");
    await page.getByRole("button", { name: "Log in" })
      .click();
    await use(page);                       // the test runs
    // teardown would go here
  },
});

testWithShopper("cart persists",
  async ({ shopper }) => { /* born logged in */ });

Start with the mystery hiding in plain sight: every test signature says async ({ page }) — 4.11’s destructuring — but no line of yours ever created a page. Something is building it and passing it in. That something is the fixture system, and it’s the runner’s deepest idea.

test("…", async ({ page }) => {↑ who made this?the RUNNER built it — a fixture,injected because you named itthe day-one mystery: { page } — you never created it. Who hands it in?
under the hood

The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”

Fixtures form a dependency GRAPH (8.1’s picture, again): your shopper uses page, page uses context, context uses browser. The runner resolves the chain lazily and tears it down in reverse order — construction and destruction as mirror images, guaranteed.

Fixtures have scopes: the default is per-test, but { scope: "worker" } builds once per worker process (11.15) — right for expensive resources safe to share across tests in one worker, like a database connection. Per-test freshness stays the default because isolation is the prize.

The UI-login-per-test shown here is honest but expensive — which is exactly why 11.11 exists: log in ONCE, bottle the session (storageState), and hand every context the bottle. The shopper fixture then becomes a one-liner that loads state instead of driving the login form. Same injection shape, hundred× cheaper.

Job note: reading an unfamiliar suite, open its fixtures.ts first — it’s the cast of characters. A suite’s maturity is visible there: good ones read like a menu of prepared worlds (shopper, admin, emptyCart, seededCatalog); bad ones have one god-fixture doing everything for everyone.

your turn

⌨️ build the fixture sandwich

The use() pattern in miniature: a fixture that sets up, hands the resource to the test, and tears down — GUARANTEED, in that order, with the test as the filling.

requirements:

  • Write shopperFixture(testFn): print setup: log in, build the resource const shopper = "logged-in page", call testFn(shopper) (the use() moment), then print teardown: log out.
  • Write two “tests” as functions: one printing test A sees: logged-in page (build it from the parameter — 3.8’s functions-as-values carry the day), one printing test B sees: logged-in page.
  • Run both through the fixture — each test gets the full sandwich: setup, filling, teardown, twice over.

when you press RUN, the console must show exactly:

setup: log in
test A sees: logged-in page
teardown: log out
setup: log in
test B sees: logged-in page
teardown: log out

✏️ Quick check 1

Test 1 sets a cookie and adds items to localStorage. What does test 2’s page see in them?

✏️ Quick check 2

In a custom fixture, code written AFTER await use(page) runs when?

✏️ Quick check 3

A test’s parameters say only ({ page }). Does the shopper fixture’s login run for it? Type yes or no.

teach it back

🗣️ Now teach it back

Solve the { page } mystery for a friend: what a fixture is, what “fresh context per test” means and why it’s cheap, hooks vs fixtures, and the use() sandwich (with why teardown-after-use beats a separate afterEach).

Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.

a few sentences, minimum — you’ve got this
to remember
Fixtures = prepared resources injected BY NAME ({ page } is 4.11 destructuring on a delivery) — 10.6’s dependency injection as architecture. Lazy: built only when asked.
Every test gets a FRESH context (cookies/storage zeroed — 7.7 reset): structural isolation, cheap because contexts are incognito-light while the browser process is shared. Prerequisite for parallelism.
Hooks = per-file beats (beforeEach = shared Arrange). Custom fixtures via extend + the use() SANDWICH: setup before, test as filling, teardown after — guaranteed even on failure, forever paired.