Fixtures with AI
Lift repeated setup into a Playwright fixture with the agent doing the typing — and decide what should and shouldn't be hidden.
POMs solved the locator-duplication problem in the previous lesson, but they introduced a new one: every test still pays the init tax up front.
test.describe("Snowboard purchase flow", () => {
test("searches for a snowboard, opens the first result, and adds it to the cart", async ({
page,
}) => {
const home = new HomePage(page);
const search = new SearchPage(page);
const product = new ProductPage(page);
const cart = new Cart(page);
// ...
});
});
That's a fixture refactoring waiting to happen.
What is a fixture?
A fixture extends Playwright's test with named values your specs can destructure — the same way page already arrives in every test. Playwright ships a handful out of the box:
| Fixture | Type | Description |
|---|---|---|
page | Page | Isolated page for this test run. |
context | BrowserContext | Isolated context for this test run. The page fixture belongs to this context as well. |
browser | Browser | Browsers are shared across tests to optimize resources. |
browserName | string | The name of the browser currently running the test. Either chromium, firefox, or webkit. |
request | APIRequestContext | Isolated APIRequestContext instance for this test run. |
…and you can add your own. Wire each POM up once in tests/base.ts:
import { test as base } from "@playwright/test";
import { HomePage } from "./poms/home-page";
import { SearchPage } from "./poms/search-page";
import { ProductPage } from "./poms/product-page";
import { Cart } from "./poms/cart";
type Fixtures = {
home: HomePage;
search: SearchPage;
product: ProductPage;
cart: Cart;
};
export const test = base.extend<Fixtures>({
home: async ({ page }, use) => use(new HomePage(page)),
search: async ({ page }, use) => use(new SearchPage(page)),
product: async ({ page }, use) => use(new ProductPage(page)),
cart: async ({ page }, use) => use(new Cart(page)),
});
Then every spec asks for the ones it needs by name:
import { test } from "./base";test("buys a snowboard", async ({ home, search, product, cart }) => { // ...});
Same four POMs, zero init lines. The fixture sets them up before the test runs and disposes of anything that needs cleanup after.
Setup, use, teardown
Each fixture body has three phases — anything before await use(...) is setup (like beforeEach), the value handed to use is what the test receives, and anything after use returns is teardown (like afterEach).
export const test = base.extend<Fixtures>({
loggedInPage: async ({ page }, use) => {
// setup
await page.goto("/login");
await page.getByLabel("Your name").fill("stefan");
await page.getByLabel("Your password").fill("12345678");
await page.getByRole("button", { name: "Sign in" }).click();
await use(page);
// teardown
await page.context().clearCookies();
},
});
The POM fixtures above are the trivial case — use(new HomePage(page)) with nothing to tear down. Logged-in pages, seeded carts, and temp files use both ends.
Let the agent do the extraction
Reading the duplicated setup, lifting it into tests/base.ts, and rewriting each spec to import from there is mechanical work. The agent reads the duplicates and proposes the refactor in seconds.
What to push back on: fixtures that hide too much. If every test silently logs in the same user, the spec stops telling the truth about what it tests. Fixtures should be obvious setup, not a magic preamble.
Hand the fixture refactor to the agent
Extract a fixture from repeated setup
Find two or three specs in your workshop project that share setup (login, cart seed, navigation to a feature page). Try drafting a prompt yourself.
Show an example prompt
Find the page object model setup steps repeated across all tests/*.spec.ts. Extract initialization
into a Playwright fixture setup in tests/base.ts (extending Playwright's base test).
Refactor the specs.ts files to import from ./base instead. Run tests to see if things still work!