Skip to main content
Back to Blog
Reference
2026-05-05

Playwright Fixtures: Complete Reference 2026

Complete reference for Playwright fixtures in 2026: built-in fixtures, custom fixtures, worker scope, options, override patterns, and TypeScript typing.

Playwright Fixtures: Complete Reference 2026

Fixtures are how Playwright moves shared setup out of every test and into a typed, reusable layer. Anything you set up before a test, tear down after, or pass into a test as a parameter is a fixture. The runner instantiates each fixture on demand, dependency-resolves its inputs, scopes its lifetime, and disposes it cleanly when the dependent tests finish. In 2026 mastering fixtures is the difference between a 200-test suite that compiles in 4 seconds and one that takes 40.

This reference covers every built-in fixture, every custom fixture pattern, every scope (test vs worker), and the override mechanics that let you bend fixtures without rewriting tests. Every example is TypeScript with strict mode and Playwright 1.49+.

For broader Playwright fundamentals, the Playwright E2E Complete Guide is the entry point. The playwright-e2e skill ensures AI assistants generate fixtures that follow these patterns.

Built-in fixtures

Every test receives access to these out of the box:

FixtureTypeScopePurpose
pagePagetestFresh Page instance
contextBrowserContexttestBrowser context for the test
browserBrowserworkerShared browser instance across tests in a worker
browserNamestringworkerOne of 'chromium', 'firefox', 'webkit'
requestAPIRequestContexttestAPI client tied to the context cookies
baseURLstring | undefinedtestThe configured base URL
viewportViewportSize | nulltestConfigured viewport
deviceScaleFactornumbertestConfigured DPR
isMobilebooleantestWhether mobile emulation is active
hasTouchbooleantestWhether touch is enabled
localestringtestConfigured locale
timezoneIdstringtestConfigured timezone
storageStatestring | StorageState | undefinedtestInitial storage state
userAgentstringtestUser-Agent header

Use them via destructuring:

import { test, expect } from '@playwright/test';

test('uses base URL', async ({ page, baseURL }) => {
  await page.goto(baseURL!);
  await expect(page).toHaveTitle(/QASkills/);
});

Defining custom fixtures

Use test.extend to add fixtures. The first generic is the new fixture type.

import { test as base, expect } from '@playwright/test';

type Fixtures = {
  todoList: { add: (text: string) => Promise<void> };
};

export const test = base.extend<Fixtures>({
  todoList: async ({ page }, use) => {
    await page.goto('/todos');
    const add = async (text: string) => {
      await page.getByLabel('New todo').fill(text);
      await page.keyboard.press('Enter');
    };
    await use({ add });
    // teardown after test
    await page.evaluate(() => localStorage.clear());
  },
});

export { expect };
import { test, expect } from './fixtures';

test('adds a todo', async ({ todoList, page }) => {
  await todoList.add('Buy milk');
  await expect(page.getByText('Buy milk')).toBeVisible();
});

The function receives the existing fixtures ({ page } here), a use callback, and returns nothing. Code before use is setup; code after use is teardown.

Fixture scopes

Fixtures default to test scope: one instance per test. Set scope: 'worker' to share across all tests in a worker.

type Fixtures = {
  expensiveResource: { client: SomeClient };
};

export const test = base.extend<{}, Fixtures>({
  expensiveResource: [
    async ({}, use) => {
      const client = await SomeClient.connect();
      await use({ client });
      await client.close();
    },
    { scope: 'worker' },
  ],
});

Worker-scoped fixtures live as long as the worker process. Use them for database connections, browser instances, or any setup whose cost amortizes across many tests.

Automatic fixtures

A fixture annotated auto: true runs for every test, even when no test destructures it. Useful for global setup like preventing console errors.

type Fixtures = {
  failOnConsoleError: void;
};

export const test = base.extend<Fixtures>({
  failOnConsoleError: [
    async ({ page }, use) => {
      const errors: string[] = [];
      page.on('console', (msg) => {
        if (msg.type() === 'error') errors.push(msg.text());
      });
      await use();
      expect(errors).toEqual([]);
    },
    { auto: true },
  ],
});

Every test now fails if the page logs an error, without each spec needing to opt in.

Option fixtures

Options are configuration values that can be overridden per project or per file. Declare with [defaultValue, { option: true }].

type Options = {
  todoCount: number;
};

export const test = base.extend<Options>({
  todoCount: [10, { option: true }],
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'small', use: { todoCount: 5 } },
    { name: 'large', use: { todoCount: 100 } },
  ],
});

Within a test, override per spec with test.use:

test.use({ todoCount: 25 });

Override built-in fixtures

You can override the built-in page fixture to wrap every test's page in custom behavior, for example to inject auth.

import { test as base } from '@playwright/test';

export const test = base.extend({
  page: async ({ page }, use) => {
    await page.addInitScript(() => {
      window.localStorage.setItem('access_token', 'test-token');
    });
    await use(page);
  },
});

Every test that uses the new test import inherits the override.

Authentication fixtures

The canonical example: log in once per worker and reuse the storage state.

// auth.setup.ts
import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  await page.context().storageState({ path: authFile });
});
// playwright.config.ts
projects: [
  { name: 'setup', testMatch: /auth\.setup\.ts/ },
  {
    name: 'logged-in',
    use: { storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup'],
  },
],

Every test in logged-in starts authenticated. The setup runs once per CI invocation.

Per-user authentication

For multi-role tests, define one fixture per role.

type Fixtures = {
  adminPage: Page;
  memberPage: Page;
};

export const test = base.extend<Fixtures>({
  adminPage: async ({ browser }, use) => {
    const context = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },
  memberPage: async ({ browser }, use) => {
    const context = await browser.newContext({ storageState: 'playwright/.auth/member.json' });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },
});
test('admin can delete user', async ({ adminPage }) => {
  await adminPage.goto('/users');
  await adminPage.getByRole('button', { name: 'Delete' }).first().click();
});

test('member sees no delete option', async ({ memberPage }) => {
  await memberPage.goto('/users');
  await expect(memberPage.getByRole('button', { name: 'Delete' })).toHaveCount(0);
});

Database fixtures

Set up isolated test data per test or per worker.

type Fixtures = {
  testUser: { id: string; email: string };
};

export const test = base.extend<Fixtures>({
  testUser: async ({}, use, workerInfo) => {
    const user = await db.createUser({
      email: `user-${workerInfo.workerIndex}-${Date.now()}@example.com`,
    });
    await use(user);
    await db.deleteUser(user.id);
  },
});

workerInfo gives you a per-worker index, useful for partitioning data so concurrent workers do not collide.

Reusing fixtures across files

Define fixtures in one file and import everywhere.

// fixtures/index.ts
import { test as base, expect } from '@playwright/test';
import type { Page } from '@playwright/test';
import { CheckoutPage } from '../pages/CheckoutPage';

type Fixtures = {
  checkout: CheckoutPage;
};

export const test = base.extend<Fixtures>({
  checkout: async ({ page }, use) => {
    const checkout = new CheckoutPage(page);
    await use(checkout);
  },
});

export { expect };
// tests/checkout.spec.ts
import { test, expect } from '../fixtures';

test('user completes checkout', async ({ checkout }) => {
  await checkout.fillContact('Asha', 'asha@example.com');
  await checkout.placeOrder();
  await expect(checkout.orderConfirmed).toBeVisible();
});

Composing multiple extend calls

You can extend an extended test to layer fixtures from different modules.

import { test as authTest } from './auth-fixtures';
import { test as dbTest } from './db-fixtures';
// Note: in real code, merge the types yourself or pick one base

The mergeTests utility in @playwright/test 1.40+ handles this cleanly:

import { mergeTests } from '@playwright/test';
import { test as authTest } from './auth-fixtures';
import { test as dbTest } from './db-fixtures';

export const test = mergeTests(authTest, dbTest);

Timeouts on fixtures

By default a fixture inherits the test's timeout. Override for slow setups.

export const test = base.extend({
  slowResource: [
    async ({}, use) => {
      const resource = await spinUp();
      await use(resource);
      await resource.close();
    },
    { timeout: 60_000 },
  ],
});

Fixture failures and testInfo

Fixtures receive TestInfo if declared as the third parameter. Use it to attach files or read configuration.

export const test = base.extend({
  capturedScreenshot: async ({ page }, use, testInfo) => {
    await use();
    if (testInfo.status !== testInfo.expectedStatus) {
      await testInfo.attach('screenshot', {
        body: await page.screenshot(),
        contentType: 'image/png',
      });
    }
  },
});

The attached file shows up in the HTML report.

Common pitfalls

Pitfall 1: Forgetting await use(). A fixture must call use() to hand off to the test. Without it, the test never runs.

Pitfall 2: Sharing mutable state without scoping. A test-scoped fixture that mutates a worker-scoped resource needs explicit cleanup, or tests see each other's leftovers.

Pitfall 3: Heavy fixtures at test scope. A 2-second-to-spin-up resource at test scope multiplies cost by N tests. Promote to worker scope.

Pitfall 4: Circular fixtures. Two fixtures that depend on each other never resolve. Inline one or split the responsibility.

Pitfall 5: Importing the wrong test. When you build test.extend, you must re-export and import the new test. Tests that import the original @playwright/test do not see your fixtures.

Anti-patterns

  • Putting test data in fixtures and asserting on it from many tests. Tests should declare their own data unless the fixture is genuinely shared.
  • Mixing setup and assertions in a fixture. Fixtures provide; tests assert.
  • Reaching into process.env from inside a fixture without an option fallback. Make fixtures testable in isolation.
  • Stateful worker-scoped fixtures without idempotent cleanup. A test that fails mid-fixture leaves state behind.

Fixture lifecycle diagram

PhaseWhat runs
Worker startupWorker-scoped fixtures' setup, in dependency order
Test discoveryNone
For each testTest-scoped fixtures' setup, then test body
After testTest-scoped fixtures' teardown (reverse order)
Worker shutdownWorker-scoped fixtures' teardown

Failures in setup propagate as test failures with the fixture's stack trace; failures in teardown propagate as separate errors in the report.

Conclusion and next steps

Fixtures are the secret to Playwright suites that scale. Default to test scope, promote to worker when cost demands, lean on options for project-level variation, and treat each fixture as a single-purpose unit.

Install the playwright-e2e skill so AI assistants generate fixtures that respect these patterns. For broader test config, read the Playwright Test Config Options Complete Reference. For sharing context state, see Playwright Browser Contexts Isolation Guide.

Playwright Fixtures: Complete Reference 2026 | QASkills.sh