Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
core/fixtures-hooks.md
1# Fixtures & Hooks23## Table of Contents451. [Built-in Fixtures](#built-in-fixtures)62. [Custom Fixtures](#custom-fixtures)73. [Fixture Scopes](#fixture-scopes)84. [Hooks](#hooks)95. [Authentication Patterns](#authentication-patterns)106. [Database Fixtures](#database-fixtures)1112## Built-in Fixtures1314### Core Fixtures1516```typescript17test("example", async ({18page, // Isolated page instance19context, // Browser context (cookies, localStorage)20browser, // Browser instance21browserName, // 'chromium', 'firefox', or 'webkit'22request, // API request context23}) => {24// Each test gets fresh instances25});26```2728### Request Fixture2930```typescript31test("API call", async ({ request }) => {32const response = await request.get("/api/users");33await expect(response).toBeOK();3435const users = await response.json();36expect(users).toHaveLength(5);37});38```3940## Custom Fixtures4142### Basic Custom Fixture4344```typescript45// fixtures.ts46import { test as base } from "@playwright/test";4748// Declare fixture types49type MyFixtures = {50todoPage: TodoPage;51apiClient: ApiClient;52};5354export const test = base.extend<MyFixtures>({55// Fixture with setup and teardown56todoPage: async ({ page }, use) => {57const todoPage = new TodoPage(page);58await todoPage.goto();5960await use(todoPage); // Test runs here6162// Teardown (optional)63await todoPage.clearTodos();64},6566// Simple fixture67apiClient: async ({ request }, use) => {68await use(new ApiClient(request));69},70});7172export { expect } from "@playwright/test";73```7475### Fixture with Options7677```typescript78type Options = {79defaultUser: { email: string; password: string };80};8182type Fixtures = {83authenticatedPage: Page;84};8586export const test = base.extend<Options & Fixtures>({87// Define option with default88defaultUser: [89{ email: "[email protected]", password: "pass123" },90{ option: true },91],9293// Use option in fixture94authenticatedPage: async ({ page, defaultUser }, use) => {95await page.goto("/login");96await page.getByLabel("Email").fill(defaultUser.email);97await page.getByLabel("Password").fill(defaultUser.password);98await page.getByRole("button", { name: "Sign in" }).click();99await use(page);100},101});102103// Override in config104export default defineConfig({105use: {106defaultUser: { email: "[email protected]", password: "admin123" },107},108});109```110111### Automatic Fixtures112113```typescript114export const test = base.extend<{}, { setupDb: void }>({115// Auto-fixture runs for every test without explicit usage116setupDb: [117async ({}, use) => {118await seedDatabase();119await use();120await cleanDatabase();121},122{ auto: true },123],124});125```126127## Fixture Scopes128129### Test Scope (Default)130131Created fresh for each test:132133```typescript134test.extend({135page: async ({ browser }, use) => {136const page = await browser.newPage();137await use(page);138await page.close();139},140});141```142143### Worker Scope144145Shared across tests in the same worker (each worker gets its own instance; tests in different workers do not share it):146147```typescript148type WorkerFixtures = {149sharedAccount: Account;150};151152export const test = base.extend<{}, WorkerFixtures>({153sharedAccount: [154async ({ browser }, use) => {155// Expensive setup - runs once per worker156const account = await createTestAccount();157await use(account);158await deleteTestAccount(account);159},160{ scope: "worker" },161],162});163```164165### Isolate test data between parallel workers166167When tests in different workers touch the same backend or DB (e.g. same user, same tenant), they can collide and cause flaky failures. Use `testInfo.workerIndex` (or `process.env.TEST_WORKER_INDEX`) in a worker-scoped fixture to create unique data per worker:168169```typescript170import { test as baseTest } from "@playwright/test";171172type WorkerFixtures = {173dbUserName: string;174};175176export const test = baseTest.extend<{}, WorkerFixtures>({177dbUserName: [178async ({}, use, testInfo) => {179const userName = `user-${testInfo.workerIndex}`;180await createUserInTestDatabase(userName);181await use(userName);182await deleteUserFromTestDatabase(userName);183},184{ scope: "worker" },185],186});187```188189Then each worker uses a distinct user (e.g. `user-1`, `user-2`), so parallel workers do not overwrite each other’s data.190191## Hooks192193### beforeEach / afterEach194195```typescript196test.beforeEach(async ({ page }) => {197// Runs before each test in file198await page.goto("/");199});200201test.afterEach(async ({ page }, testInfo) => {202// Runs after each test203if (testInfo.status !== "passed") {204await page.screenshot({ path: `failed-${testInfo.title}.png` });205}206});207```208209### beforeAll / afterAll210211```typescript212test.beforeAll(async ({ browser }) => {213// Runs once before all tests in file214// Note: Cannot use page fixture here215});216217test.afterAll(async () => {218// Runs once after all tests in file219});220```221222### Describe-Level Hooks223224```typescript225test.describe("User Management", () => {226test.beforeEach(async ({ page }) => {227await page.goto("/users");228});229230test("can list users", async ({ page }) => {231// Starts at /users232});233234test("can add user", async ({ page }) => {235// Starts at /users236});237});238```239240## Authentication Patterns241242### Global Setup with Storage State243244```typescript245// auth.setup.ts246import { test as setup, expect } from "@playwright/test";247248const authFile = ".auth/user.json";249250setup("authenticate", async ({ page }) => {251await page.goto("/login");252await page.getByLabel("Email").fill(process.env.TEST_EMAIL!);253await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!);254await page.getByRole("button", { name: "Sign in" }).click();255256await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();257await page.context().storageState({ path: authFile });258});259```260261```typescript262// playwright.config.ts263export default defineConfig({264projects: [265{ name: "setup", testMatch: /.*\.setup\.ts/ },266{267name: "chromium",268use: {269...devices["Desktop Chrome"],270storageState: ".auth/user.json",271},272dependencies: ["setup"],273},274],275});276```277278### Multiple Auth States279280```typescript281// auth.setup.ts282setup("admin auth", async ({ page }) => {283await login(page, "[email protected]", "adminpass");284await page.context().storageState({ path: ".auth/admin.json" });285});286287setup("user auth", async ({ page }) => {288await login(page, "[email protected]", "userpass");289await page.context().storageState({ path: ".auth/user.json" });290});291```292293```typescript294// playwright.config.ts295projects: [296{297name: "admin tests",298testMatch: /.*admin.*\.spec\.ts/,299use: { storageState: ".auth/admin.json" },300dependencies: ["setup"],301},302{303name: "user tests",304testMatch: /.*user.*\.spec\.ts/,305use: { storageState: ".auth/user.json" },306dependencies: ["setup"],307},308];309```310311### Auth Fixture312313```typescript314// fixtures/auth.fixture.ts315export const test = base.extend<{ adminPage: Page; userPage: Page }>({316adminPage: async ({ browser }, use) => {317const context = await browser.newContext({318storageState: ".auth/admin.json",319});320const page = await context.newPage();321await use(page);322await context.close();323},324325userPage: async ({ browser }, use) => {326const context = await browser.newContext({327storageState: ".auth/user.json",328});329const page = await context.newPage();330await use(page);331await context.close();332},333});334```335336## Database Fixtures337338This section covers **per-test database fixtures** (isolation, transaction rollback). For related topics:339340- **Test data factories** (builders, Faker): See [test-data.md](test-data.md)341- **One-time database setup** (migrations, snapshots): See [global-setup.md](global-setup.md#database-patterns)342343### Transaction Rollback Pattern344345```typescript346import { test as base } from "@playwright/test";347import { db } from "../db";348349export const test = base.extend<{ dbTransaction: Transaction }>({350dbTransaction: async ({}, use) => {351const transaction = await db.beginTransaction();352353await use(transaction);354355await transaction.rollback(); // Clean slate for next test356},357});358```359360### Seed Data Fixture361362```typescript363type TestData = {364testUser: User;365testProducts: Product[];366};367368export const test = base.extend<TestData>({369testUser: async ({}, use) => {370const user = await db.users.create({371email: `test-${Date.now()}@example.com`,372name: "Test User",373});374375await use(user);376377await db.users.delete(user.id);378},379380testProducts: async ({ testUser }, use) => {381const products = await db.products.createMany([382{ name: "Product A", ownerId: testUser.id },383{ name: "Product B", ownerId: testUser.id },384]);385386await use(products);387388await db.products.deleteMany(products.map((p) => p.id));389},390});391```392393## Fixture Tips394395| Tip | Explanation |396| ------------------ | ------------------------------------------- |397| Fixtures are lazy | Only created when used |398| Compose fixtures | Use other fixtures as dependencies |399| Keep setup minimal | Do heavy lifting in worker-scoped fixtures |400| Clean up resources | Use teardown in fixtures, not afterEach |401| Avoid shared state | Each fixture instance should be independent |402403## Anti-Patterns to Avoid404405| Anti-Pattern | Problem | Solution |406| ----------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |407| Shared mutable state between tests | Race conditions, order dependencies | Use fixtures for isolation |408| Global variables in tests | Tests depend on execution order | Use fixtures or beforeEach for setup |409| Not cleaning up test data | Tests interfere with each other | Use fixtures with teardown or database transactions |410| Shared `page` or `context` in `beforeAll` | State leak between tests; flaky when tests run in parallel | Use default one-context-per-test, or `beforeEach` + fresh page; if serial is required, prefer `test.describe.configure({ mode: 'serial' })` and document that isolation is sacrificed |411| Backend/DB state shared across workers | Tests in different workers collide on same data | Use worker-scoped fixture with `testInfo.workerIndex` to create unique data per worker |412413## Related References414415- **Page Objects with fixtures**: See [page-object-model.md](page-object-model.md) for POM patterns416- **Test organization**: See [test-suite-structure.md](test-suite-structure.md) for test structure417- **Debugging fixture issues**: See [debugging.md](../debugging/debugging.md) for troubleshooting418