Table of Contents Link to heading

Working example Link to heading

You can find a working example in my repository pw-ts-fixtures

Playwright Fixtures with TypeScript Link to heading

Playwright is a powerful end-to-end testing framework that supports modern web apps. One of its most useful features is fixtures, which allow you to set up and tear down resources for your tests in a clean, reusable way. When combined with TypeScript, fixtures become even more robust and type-safe.

What Are Fixtures? Link to heading

Fixtures in Playwright are reusable pieces of setup and teardown logic that can be shared across your tests. They help you:

  • Prepare the environment before tests run (all code that is before use)
  • Share data or resources between tests (data in use(data))
  • Clean up after tests (all code that is after use)

Benefits of Using Fixtures Link to heading

  • Reusability: Write setup logic once and use it across multiple tests.
  • Isolation: Each test gets a fresh setup, reducing flakiness.
  • Type Safety: With TypeScript, you get autocompletion and type checking for your fixtures.

Playwright Built-in Fixtures Link to heading

Playwright comes with a set of built-in fixtures that make our lives easier. If you have been using Playwright, you are probably familiar with:

  • page: Provides a new browser page (tab) for each test. This is the main way to interact with your web application.
  • request: Lets you make HTTP requests directly from your tests, useful for API setup or validation.

These built-in fixtures can be used directly in your test functions by mentioning them as arguments:

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

test("basic test", async ({ page }) => {
  await page.goto("https://example.com");
  await expect(page).toHaveTitle(/Example/);
});

test("API test", async ({ request }) => {
  const response = await request.get("https://api.example.com/data");
  expect(response.ok()).toBeTruthy();
});

Other Playwright built-in fixtures:

  • browser: Gives access to the browser instance itself, allowing you to create new pages or contexts.
  • context: Provides a browser context, which is an isolated environment for pages (useful for multi-user scenarios).
  • testInfo: Contains information about the currently running test, such as its title, status, and output directory.

You can also override or extend these built-in fixtures to customize their behavior for your own needs. And today, we will talk about that :)

How to Set Up Your Own Fixture Link to heading

I use file-name.fixture.ts filenames for custom fixtures, but it is not obligatory—just a convention that we use in my team.

// /src/fixtures/my-mock-data.fixture.ts
// The tricky part: you are going to create a new test based on Playwright's test,
// so you have to use an alias import like this
import { test as base } from "@playwright/test";

// Declare the types of your fixtures
interface MyFixtures {
  someMockData: MockData;
}

interface MockData {
  id: number;
  name: string;
  body: string;
}
// Extend base test by providing "someMockData"
// This new "test" can be used in multiple test files, and each of them
// will get the fixtures.
export const test = base.extend<MyFixtures>({
  someMockData: async ({}, use) => {
    // Set up the fixture.
    console.log("someMockData fixture is going to be set up");
    const data: MockData = {
      id: 1,
      name: "some name",
      body: "some body",
    };

    await use(data); // this is the data that is going to be used in your tests

    // here you can clean up the fixture after the test
    console.log("myData fixture is going to clean up after the test");
  },
});

And that’s it! You can now use it in your test—just import test from the newly created fixture, instead of the Playwright package.

import { test } from "@fixtures/example-fixture";
import { expect } from "@playwright/test";

test("use your own mock data from the fixture", ({ someMockData }) => {
  console.log(someMockData);

  expect(someMockData).toBeDefined();
  expect(someMockData.id).toBe(1);
  expect(someMockData.name).toBe("some name");
});

This fixture is quite simple but shows how it works without unnecessary complexity:

Fixture Execution Flow

Common Use Cases for Fixtures Link to heading

1. Setting Up a Page to a Specific State Link to heading

You can use fixtures to navigate a browser page to a certain URL, log in a user, or perform any setup steps required before your tests begin. This ensures that each test starts from a known state.

const test = base.extend({
  loggedInPage: async ({ page }, use) => {
    // this is what happens before the test
    await page.goto("https://example.com/login");
    await page.fill("#username", "user");
    await page.fill("#password", "pass");
    await page.click('button[type="submit"]');

    await use(page); // this is returned to the test

    // everything after use is executed after the test
    await page.close();
  },
});

2. Downloading Data from an API or Setting Up Accounts Link to heading

Fixtures can fetch data from an API or set up test accounts before your tests run. This is useful for seeding your tests with real or mock data.

const test = base.extend({
  apiData: async ({ request }, use) => {
    const response = await request.get("https://api.example.com/data");
    const data = await response.json();
    await use(data);
  },
});

3. Returning Data Objects (e.g., API Responses) Link to heading

Fixtures can return any data you need for your tests, such as objects fetched from an API. This makes your tests more modular and easier to maintain.

interface User {
  id: number;
  name: string;
}

const test = base.extend<{ user: User }>({
  user: async ({ request }, use) => {
    const response = await request.get("https://api.example.com/user/1");
    const user = await response.json();
    await use(user);
  },
});

Example: Combining Fixtures Link to heading

You can combine multiple fixtures to set up complex test scenarios. For example, you might fetch some API data and also prepare a page:

import type { APIPosts } from "./api-post";
import { test as base, expect, Page } from "@playwright/test";

interface MyFixtures {
  someAPIData: APIPosts;
  examplePage: Page;
}

const test = base.extend<MyFixtures>({
  someAPIData: async ({ request }, use) => {
    // Set up the fixture.
    console.log("myData fixture is going to be set up");

    const response = await request.get(API_URL_POSTS);

    expect(response.status()).toBe(200);
    const data = (await response.json()) as APIPosts;

    await use(data); // this is the data that is going to be used in your tests

    // here you can clean up the fixture after the test
    console.log("myData fixture is going to clean up after the test");
  },

  examplePage: async ({ page, someAPIData }, use) => {
    // you can set up some backend state here using other custom fixtures
    console.log("examplePage fixture is going to be set up");
    console.log("Data from other fixture:");
    console.log(someAPIData[0]);

    await page.goto("https://example.com");

    await use(page);

    await page.close();

    console.log("examplePage fixture is going to be cleaned up");
  },
});

Fixture Execution Flow - Multiple Fixtures

Using Multiple Fixtures from Different Files in Your Test Link to heading

import { test as additionalFixture } from "@fixtures/additional.fixture";
import { test as exampleFixture } from "@fixtures/my-example.fixture";
import { expect, mergeTests } from "@playwright/test";

const test = mergeTests(exampleFixture, additionalFixture);

Worker-scoped Fixtures Link to heading

By default, fixtures in Playwright are test-scoped, but you can change them to be worker-scoped if you want to avoid unnecessary API calls or object creation. Here is an example of using it and the results of executing a worker-scoped fixture.

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

interface MyFixtures {
  someMockData: MockData;
}

interface MockData {
  id: number;
  name: string;
  body: string;
}

// The tuple's second parameter is used to define the scope of the fixture - { scope: "worker" }
export const test = base.extend<MyFixtures>({
  someMockData: [
    async ({}, use, workerInfo): Promise<void> => {
      console.log(`worker index: ${workerInfo.workerIndex}`);
      console.log("someMockData fixture is going to be set up");
      const data: MockData = {
        id: 1,
        name: "some name",
        body: "some body",
      };

      await use(data);

      console.log("someMockData fixture is going to clean up after the test");
    },
    // The second parameter of the tuple contains an object with the option { scope: "worker" } - by default it is set to "test"
    { scope: "worker" },
  ],
});

Single worker:

worker-scoped fixture example - single worker

Multiple workers:

worker-scoped fixture example - multiple workers

Auto Fixtures Link to heading

These fixtures are executed even if you don’t pass them directly to your tests; it is enough to use test that includes fixtures with { auto: true}. They are really handy, for example, if you want to collect some logs when tests fail or prepare custom reports.

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

export const test = base.extend<{ doSomeAction: void }>({
  doSomeAction: [
    async ({}, use, testInfo): Promise<void> => {
      console.log("doSomeAction fixture is going to be set up");

      await use();

      if (testInfo.status === "failed") {
        console.log("do something when test fails");
      } else {
        console.log("do something when test passes");
      }
      console.log(
        "doSomeAction fixture is going to be clean up after the test"
      );
    },

    { auto: true },
  ],
});

Example test code:

test("auto fixture example - passing test", { tag: ["@auto-fixture"] }, () => {
  console.log("My super passing test");
});

test("auto fixture example - failing test", { tag: ["@auto-fixture"] }, () => {
  console.log("My super failing test");

  expect(true).toBe(false);
});

Execution example:

auto fixture - execution example

Conclusion Link to heading

By leveraging fixtures, you can write cleaner, more maintainable, and more reliable tests for your web applications. Playwright fixtures with TypeScript are a powerful way to manage setup and teardown logic in your tests. They help you:

  • Set up pages to a specific state.
  • Control your services, for example by setting a specific state using test APIs before test execution.
  • Download or prepare data from APIs.
  • Return data objects for use in your tests.
  • Prepare custom extra loggers that use auto-fixtures.

And many more… the sky is the limit here ^^