TutorialsJul 15, 202510 min read

Playwright fixtures: A deep dive

Waweru Mwaura

Software Engineer

Fixtures may be one of Playwright’s most powerful yet under-used features. Playwright fixtures can be used to simplify repetitive setup or teardown in your tests, manage test data ,and test state better. Fixtures are key if your objective is to write cleaner, maintainable and manageable Playwright tests.

This tutorial is aimed at helping you master using Playwright fixtures, understand their purpose, and showing how you can use them most effectively in your tests.

Prerequisites

Before starting this tutorial, make sure you have the following set up:

  • Node.js and package manager: Use npm and Node.js 18
  • Playwright Test Runner/Installation
  • Basic Playwright knowledge: Basic knowledge of writing simple Playwright tests is required.
  • Version control & CI: A code versioning repository (e.g, GitHub, GitLab, etc.) and a CircleCI account. Familiarity with YAML configs will be helpful here.
  • Git installed

Getting started

  1. First, create a Playwright test project and install the Playwright browsers:
 npm install -D @playwright/test && npx playwright install
  1. Next, clone the repository of this tutorial from Git. Cloning the repository will help you easily follow up on the tutorial steps. Run:
git clone https://github.com/CIRCLECI-GWP/playwright-fixtures.git

Understanding Playwright fixtures

What are fixtures? Imagine preparing your test suite with every test requiring a new set of test data, a clean environment with no sessions existing on the browser, and also pages or even a certain desired state before tests start executing. There are two options, one involves manually setting all this up as functions in every test file, and the other involves using Playwright fixtures.

Fixtures behave like an orchestra conductor by coordinating the setup and cleanup of every individual test where they are used. Fixtures help you with:

  • Context provision: Fixtures ensure that there is an environment and also data before the tests execute.
  • Setup and teardown encapsulation: Fixtures ensure that any setup activities or teardown are handled by the fixture body ensuring that the test only focusses on the execution logic.
  • Can be injected in tests: Fixtures just need to be declared as arguments within a test and Playwright automatically identifies and calls them within the tests that they are called in.
  • Reduce repetition: Fixtures ensure that the DRY (Don’t Repeat Yourself) principles are upheld as they serve to ensure that once declared, they can be utilized within all the tests in Playwright.
  • Increase readability: Fixtures ensure that tests are cleaner and easily readable as the abstracted implementation reduces test complexity.

The following diagram shows the interaction of fixtures with Playwright tests, and the role that they play.

Playwright fixtures

The diagram shows that the fixture holds the first order of execution in any given Playwright test; it is this that makes it so useful for set-up tasks.

Built in vs. custom fixtures

Without your knowledge, you might actually already be familiar with some fixtures, and may have even used some of them. In this section you will explore some of the standard fixtures in Playwright and also how you can create user-defined fixtures for your own use cases.

Built in fixtures

Playwright provides some fixtures out of the box:

  • page is Playwright’s interface to ensure that the worker can interact with the browser tab.
  • context is an equivalent of an incognito browser tab and is mostly referenced as a browser context. Any pages that have a similar browser context share browser items such as cookies, local storage etc. You can however, create multiple browser contexts even within a test and this ensure that tests requiring state isolation can be executed.
  • browser is Playwright’s instance of a browser including Chromium browsers, Webkit, and even Firefox.
  • browserName is Playwright’s configuration of the browser that has been configured to execute the tests. It is a string definition.
  • request is Playwright’s context of an API request that is pre-configured to make API calls for tests that require to make API calls to the backend. This is also useful for testing API tests.

An example of using of the page fixture in a test:

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

test('should display the Playwright homepage', async ({ page }) => {
  // `page` is provided by Playwright as a default fixture
  await page.goto('https://playwright.dev/');
  await expect(page.locator('nav >> text=Playwright')).toBeVisible();
});

Once the test calls the page fixture, Playwright handles its creation (a new browser tab) and returns that to the test.

If you want to have browser isolation among your tests you’ll need to follow a different example. Instead of page, use the browser fixture to create a new context in your test:

test('use browser fixture directly', async ({ browser }) => {
  const context = await browser.newContext(); 
  // Creating a new browser context
  const page = await context.newPage(); 
  // Creating a new tab from the context
  await page.goto('https://example.com'); 
  // Navigating to the url using the browser tab
  await expect(page).toHaveTitle(/Example Domain/);
});

In the test above, instead of using the provided browser in the current context, you can use the browser context to create a new context and separate the tests’ execution context to achieve test/browser isolation.

Note: You can access the test code of the built-in fixture tests under the tests subdirectory of the cloned Git repository.

Custom fixtures: Creating your own fixtures

Built-in fixtures may not work when you need to execute custom actions. You can create your own, custom fixtures using these guidelines:

  1. Repetition in setup: For any test suite that has repetitive actions before test execution you should consider a custom fixture (for example, requiring user login before your tests).
  2. Resource sharing: Fixtures can also be very handy where there is need to initialize the state of a database for your tests.
  3. Helper utils: Multiple test that have a pre-configured API client or that share an instance of a Page Object model can also be very good candidates for custom fixtures.
  4. Data management: Custom fixtures can be helpful when you need to fetch unique test data for individual tests on every run.

Now that you know what conditions make it perfect to implement custom fixtures, you can create your first custom fixture.

All fixtures follow a simple definition, accessed in the fixtures.js file in the root folder of the cloned repository.

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

const test = base.test.extend({
  // Fixture definition for myCustomFixture
  myCustomFixture: async ({}, use) => {
     // custom fixture name
    console.log('Setting up myCustomFixture...');
    const fixtureValue = 'Data from my custom fixture';
    await use(fixtureValue);
    console.log('Tearing down myCustomFixture...');
  },

  anotherFixture: async ({}, use) => {
    // Fixture definition for anotherFixture
    const data = { id: 1, name: 'Sample User' };
    await use(data);
  }
});

export { expect } from '@playwright/test';

This fixture file creates two fixtures:

  • myCustomFixture
  • anotherFixture

Both use the same setup for Playwright fixtures: you set the fixture up, carry out actions in the fixture, and then tear down the fixture. You need to always pass in the data that the fixture needs using the method use(fixtureData). If you don’t it will not be accessible to the test. Also when using fixtures, use the expect exported from your fixture file, instead of using th playwright default import.

Now, review how these two fixtures work in a Playwright test. You can find the test in the custom-fixtures.spec.js file, located in the tests/ subdirectory in the cloned repository.

// tests/custom-fixtures.spec.js
import { expect, test } from "../fixtures"; // Import from your custom fixtures file

test.describe("Custom Fixture Tests", () => {
  test("should use my custom fixture", async ({
    myCustomFixture,
    anotherFixture,
  }) => {
    console.log("Inside the test!");
    expect(myCustomFixture).toBe("Data from my custom fixture");
    console.log(
      `Received from anotherFixture: ID=${anotherFixture.id}, Name=${anotherFixture.name}`
    );
    expect(anotherFixture.name).toBe("Sample User");
  });

  test("another test using the same custom fixture", async ({
    myCustomFixture,
  }) => {
    expect(myCustomFixture.includes("custom fixture")).toBeTruthy();
  });
});

These tests use myCustomFixture and anotherFixture and assert that you can retrieve the data directly passed on the fixtures in the setup process. Note that the expect method is from the fixture file, and not on the playwright import.

Run these two tests using npx playwright test tests/custom-fixtures.spec.js . Then review the following logs on the console

Running 2 tests using 2 workers
[] › tests/custom-fixtures.spec.js:4:7 › Custom Fixture Tests › should use my custom fixture
Setting up myCustomFixture...
[] › tests/custom-fixtures.spec.js:16:7 › Custom Fixture Tests › another test using the same custom fixture
Setting up myCustomFixture...
[] › tests/custom-fixtures.spec.js:4:7 › Custom Fixture Tests › should use my custom fixture
Inside the test!
Received from anotherFixture: ID=1, Name=Sample User
[] › tests/custom-fixtures.spec.js:16:7 › Custom Fixture Tests › another test using the same custom fixture
Tearing down myCustomFixture...
[] › tests/custom-fixtures.spec.js:4:7 › Custom Fixture Tests › should use my custom fixture
Tearing down myCustomFixture...
  2 passed (3.4s)

These logs show the execution sequence of the fixtures and the time they executed. Now that you know about these, you can override built-in Playwright fixtures.

Overriding Playwright built in fixtures

Some situations might require changes to the built-in behavior of the built-in fixtures. There are specific tests that need to be created for certain projects, and Playwright caters for that. You can override the default fixture behaviour for either a single test, a group of tests, or for individual files.

One way to override the built in fixture is to have the page go to a specific page when the default page fixture is called. A browser opens a specific URL without introducing another step in the test setup. You can reference the code in the fixturesOverride.js, located in the root folder of the cloned repository.

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

export const test = base.extend({
  page: async ({ page }, use) => {
    await page.goto('https://example.com/dashboard');
    await use(page);
  }
});

export { expect } from "@playwright/test";

This fixture overrides the functionality of the page fixture and instructs it to go it to the named URL. Then, it verifies that default navigation is to the /dashboard URL using your Playwright test. Because the fixture extends the base.test class, you can call any other fixtures that you would normally call in the test() class. In this case you are calling the page class:

// tests/override-fixtures.spec.js
import { test, expect } from "../fixturesOverride";

test("should start on the dashboard page", async ({ page }) => {
  await expect(page).toHaveURL(/.*dashboard/);
});

Note: This test uses the page from the fixturesOverride.js page method because that is where the override implementation is located.

You can extend other built-in fixtures like the browser. An example would be to default the launch of the Playwright tests to be on the Chromium browser for specific tests that use your fixture:

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

export const test = base.extend({
  browser: async ({ playwright }, use) => {
    const browser = await playwright.chromium.launch({
      headless: true,
      slowMo: 200,
    });
    await use(browser);
    await browser.close();
  }
});

export {expect} from  '@playwright/test';

After creating your browser fixture, create a test to check that the launched browser was indeed a Chromium browser as defined on your fixture. Also check that you can use the browser to launch pages and go to an existing URL:

// tests/override-fixtures.spec.js
import { test, expect } from './fixtures.js';

test('browser should be chromium', async ({ browser }) => {
  // Validate that browser type is chromium
  const browserTypeName = browser.browserType().name();
  expect(browserTypeName).toBe('chromium');

  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
  await context.close();
});

This test shows how to validate that the correct browser was launched and that you can run your Playwright test actions just like in any other test. From the two examples described earlier, you have learned that you can not only create custom fixtures but also override the existing built-in fixtures.

In the next section, you will integrate CI/CD to the project and use CircleCI to ensure that you can continously run your tests on every push to a remote repository.

Writing the CI pipeline configuration

In this section, you will automate the test scripts written earlier by adding the pipeline configuration for CircleCI. You have already created a folder named .circleci in the root directory. Inside that folder, there is a config.yml file. The configuration file contains this content:

version: 2.1
orbs:
  circleci-cli: circleci/circleci-cli@0.1.9
jobs:
  build:
    docker:
      - image: mcr.microsoft.com/playwright:v1.40.0-jammy
    working_directory: ~/repo
    steps:
      - checkout
      # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "package.json" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

      - run: npm install
      - run: npm run test

      - store_artifacts:
          path: ./playwright-report
          destination: playwright-report-first
      - store_artifacts:
          path: /tmp/artifacts

In this configuration, CircleCI uses a Node.js Docker image provided by Microsoft (Playwright container) and pulled from the Docker registry. It then updates the npm package manager. Next, if the cache exists, it is restored. Application dependencies are updated only when a change has been detected with restore-cache.

Setting up a project on CircleCI

If you cloned the sample project repository, it is already initialized and set up in git. However, it can be helpful to understand how your project is integrated with CircleCI, so we will go through the steps you would have needed to follow.

To set up CircleCI, you need to initialize a GitHub repository in your project by running a command in the terminal where the project is located. To initialize the Git repository:

git init

Next, create a .gitignore file in the root directory. Inside the file, add node_modules to ignore npm-generated modules and keep them from being added to your remote repository. Add a commit and then push your project to GitHub.

After pushing your repository, you can log into CircleCI and go to Projects. All the repositories associated with your GitHub username or your organization are listed, including the one you want to set up in CircleCI. For this tutorial, it is playwright-fixtures.

Select project

Click the Set Up Project button. You will be prompted about whether you have already defined the configuration file within your project. Enter the branch name (for this tutorial, you are using main). Click Set Up Project to complete the process.

This will run successfully.

Run project

Conclusion

In this tutorial, you learned how to use Playwright hooks and the huge role they play to make sure test setup processes are not bloated. You learned how to use default Playwright hooks like page and browser, and how to create your own custom fixtures to extend the already built in test functionality. You explored how to override built-in fixtures and how to extend use of the default fixture behaviour. You reviewed how to integrate CircleCI to your tests and execute them in your CI/CD. We hope you have enjoyed reading this tutorial. Until the next one, keep testing!