Playwright Fixtures & Parameterization

This chapter introduces Playwright's Fixture mechanism, how to create custom Fixtures, and techniques for parameterized testing.


What is a Fixture

A Fixture is a dependency injection mechanism for Playwright test function parameters.

Each test obtains the required Fixtures by destructuring parameters (e.g.,{ page }), and Playwright handles creation and cleanup.

Example

// page and context are built-in Fixtures
test('Using built-in Fixtures', async ({ page, context }) => {
  // Playwright automatically creates page and context
  // Automatically cleaned up after the test ends
  await page.goto('https://www.example.com/');
});

Built-in Fixture Quick Reference

FixtureTypeDescription
pagePageIndependent browser tab
contextBrowserContextBrowser context
browserBrowserBrowser instance
browserNamestringCurrent browser name
requestAPIRequestContextHTTP request client

Custom Fixtures

Viatest.extend()Create custom Fixtures to inject additional dependencies or override built-in Fixtures.

Example

// File path: tests/fixtures.ts
import { test as base, expect } from '@playwright/test';

// Define the type of the custom Fixture
type MyFixtures = {
  todoPage: { goto: () => Promise<void>; addTodo: (text: string) => Promise<void> };
  authenticatedPage: any;
};

export const test = base.extend<MyFixtures>({
  // Custom Fixture: Todo page object
  todoPage: async ({ page }, use) => {
    // Create
    const todoPage = {
      goto: async () => {
        await page.goto('https://demo.playwright.dev/todomvc');
      },
      addTodo: async (text: string) => {
        await page.getByPlaceholder('What needs to be done?').fill(text);
        await page.keyboard.press('Enter');
      },
    };
    // Inject into test
    await use(todoPage);
    // Cleanup logic (if needed)
  },

  // Custom Fixture: Authenticated page
  authenticatedPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'playwright/.auth/user.json',
    });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },
});

export { expect };

Using Custom Fixtures

Example

// File path: tests/todo.spec.ts
import { test, expect } from './fixtures';

test('Add Todo', async ({ todoPage }) => {
  await todoPage.goto();
  await todoPage.addTodo('Learn Playwright');
  await todoPage.addTodo('Master Fixtures');
  // Verify...
});

Fixture Scopes

Fixtures have three scopes, controlled by thescopeoption.

ScopeValueLifecycle
Test-level (default)scope: 'test'Created once per test
Worker-levelscope: 'worker'Created once per Worker process, shared by all tests in the same Worker

Example

// Worker-level Fixture (shared by all tests in the same Worker)
const test = base.extend({
  sharedResource: [async ({}, use) => {
    const resource = await createExpensiveResource();
    await use(resource);
    await resource.dispose();
  }, { scope: 'worker' }],  // Note the array syntax
});

Worker-level Fixtures are shared across multiple tests within the same Worker process, suitable for resources with high creation overhead such as database connections.


Overriding Built-in Fixtures

You can override built-in Fixtures to change default behavior.

Example

const test = base.extend({
  // Override the built-in page Fixture to change the default viewport
  page: async ({ baseURL, page }, use) => {
    await page.setViewportSize({ width: 1920, height: 1080 });
    await use(page);
  },

  // Override storageState
  storageState: 'playwright/.auth/admin.json',
});

Automatic Fixture Cleanup

The Fixture's cleanup logic is executed after theuse()call, similar to the effect ofafterEach.

Example

const test = base.extend({
  tempUser: async ({ request }, use) => {
    // Create a temporary user
    const resp = await request.post('/api/users', {
      data: { name: 'temp_' + Date.now() },
    });
    const user = await resp.json();

    // Inject into test
    await use(user);

    // Automatic cleanup: Delete the temporary user
    await request.delete(`/api/users/${user.id}`);
  },
});

Parameterized Tests

Playwright does not have a built-in parameterization mechanism, but tests can be dynamically generated using loops.

Example

// Data-driven test
const testCases = [
  { username: 'admin', password: 'admin123', expected: 'Dashboard' },
  { username: 'user',  password: 'user123',  expected: 'Personal Center' },
  { username: 'guest', password: 'guest123', expected: 'Visitor Page' },
];

for (const { username, password, expected } of testCases) {
  test(`User ${username}sees $ after logging in{expected}`, async ({ page }) => {
    await page.goto('/login');
    await page.getByLabel('Username').fill(username);
    await page.getByLabel('Password').fill(password);
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page.getByText(expected)).toBeVisible();
  });
}

Test Annotations

Annotations are used to add metadata to tests, which can be viewed and filtered in the reporter.

Example

test('Test with annotation', async ({ page }) => {
  // Add annotation (inside the test function body)
  test.info().annotations.push({
    type: 'issue',
    description: 'https://github.com/example/bug/123',
  });
  test.info().annotations.push({
    type: 'description',
    description: 'This test covers all branches of the login flow',
  });

  // Test logic...
});

// Using Tag (in the test name)
test('Quick smoke test @smoke @critical', async ({ page }) => {
  // You can run only this test with npx playwright test --grep "@smoke"
});
Other Extensions