Playwright Fixtures Explained Step by Step

If you are new to Playwright, fixtures may seem confusing. But once you understand them, you’ll realize they are one of the most powerful features in Playwright.

Think of fixtures as helpers that prepare everything your test needs before it starts and clean everything after it finishes.


Real-Life Example

Imagine you’re going to cook dinner.

Before cooking, you need:

  • Wash vegetables
  • Bring utensils
  • Turn on the stove
  • Prepare ingredients

After cooking, you need:

  • Clean utensils
  • Turn off the stove
  • Clean the kitchen

Instead of doing these steps every time, imagine someone does them for you.

That helper is exactly what a fixture is.


What is a Fixture?

A fixture is a function that

  • Creates something before the test
  • Gives it to the test
  • Cleans it after the test

Think of it as:

Prepare
      ↓
Run Test
      ↓
Cleanup

Without Fixtures

Imagine you have three tests.

test('Login Test', async ({ browser }) => {

    const context = await browser.newContext();

    const page = await context.newPage();

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

    // Test

    await context.close();

});

Second test

test('Search Test', async ({ browser }) => {

    const context = await browser.newContext();

    const page = await context.newPage();

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

    // Test

    await context.close();

});

Third test

test('Logout Test', async ({ browser }) => {

    const context = await browser.newContext();

    const page = await context.newPage();

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

    // Test

    await context.close();

});

Notice something?

The same code is repeated.

  • Create Context
  • Create Page
  • Open URL
  • Close Context

This is called duplicate code.

Fixtures solve this problem.


With Fixtures

Create the page only once.

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

export const test = base.extend({

    myPage: async ({ browser }, use) => {

        const context = await browser.newContext();

        const page = await context.newPage();

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

        await use(page);

        await context.close();

    }

});

Now every test becomes very small.

test('Login Test', async ({ myPage }) => {

    // use page

});

Another test

test('Search Test', async ({ myPage }) => {

    // use page

});

No duplicate setup.


Understanding the Life Cycle

A fixture always follows this order.

Fixture Starts

↓

Create Resource

↓

Give Resource to Test

↓

Test Executes

↓

Cleanup Resource

↓

Fixture Ends

The Most Important Line

Inside every fixture you’ll see

await use(page);

This is the heart of every fixture.

It separates

Setup from Cleanup

Everything before

await use(page);

is setup.

Everything after

await use(page);

is cleanup.

Example

myPage: async ({ browser }, use) => {

    console.log("Before Test");

    const page = await browser.newPage();

    await use(page);

    console.log("After Test");

    await page.close();

}

Output

Before Test

Test Runs

After Test

Visual Timeline

Fixture Starts

↓

Open Browser

↓

Create Page

↓

Open Website

↓

await use(page)

↓

Test Starts

↓

Test Ends

↓

Close Browser

↓

Fixture Ends

What is “use”?

Suppose someone gives you a laptop.

Friend
   ↓
Hands Laptop
   ↓
You Work
   ↓
Return Laptop

The laptop is available only while you are working.

Playwright does the same thing.

Fixture

↓

Creates Page

↓

use(page)

↓

Test Uses Page

↓

Fixture Gets Control Again

↓

Cleanup

Built-in Fixtures

Playwright already provides many fixtures.

Example

test('Example', async ({ page }) => {

});

Where did page come from?

Playwright created it automatically.

Built-in fixtures include

FixturePurpose
browserBrowser instance
pageNew page
contextBrowser context
requestAPI testing
browserNameBrowser name

Custom Fixture

You can create your own fixture.

Example

export const test = base.extend({

    username: async ({}, use) => {

        await use("Deepesh");

    }

});

Use it

test('Example', async ({ username }) => {

    console.log(username);

});

Output

Deepesh

The fixture can return anything.

  • String
  • Number
  • Object
  • Page
  • Database Connection
  • API Client
  • Login Session

Fixture Returning an Object

user: async ({}, use) => {

    await use({

        name: "Deepesh",

        role: "Admin"

    });

}

Use

test('Example', async ({ user }) => {

    console.log(user.name);

    console.log(user.role);

});

Output

Deepesh

Admin

Login Fixture Example

Instead of logging in inside every test,

Create a login fixture.

loginPage: async ({ page }, use) => {

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

    await page.fill("#username", "admin");

    await page.fill("#password", "admin123");

    await page.click("button");

    await use(page);

}

Now every test starts after login.

test('Dashboard Test', async ({ loginPage }) => {

    await loginPage.click("text=Dashboard");

});

Database Fixture Example

db: async ({}, use) => {

    const database = connectDB();

    await use(database);

    database.close();

}

Every test gets the database connection.


API Fixture Example

api: async ({ request }, use) => {

    const response = request;

    await use(response);

}

Worker Fixture vs Test Fixture

There are two types of fixtures.

Test Fixture

Created for every test.

Test1

Create

↓

Run

↓

Destroy

----------------

Test2

Create

↓

Run

↓

Destroy

Worker Fixture

Created only once for a worker (a process that runs tests).

Worker Starts

↓

Create Fixture

↓

Test1

↓

Test2

↓

Test3

↓

Destroy Fixture

↓

Worker Ends

Worker fixtures are useful for expensive setup, such as:

  • Starting a database
  • Connecting to a server
  • Creating a shared API client
  • Loading large configuration files

Fixture Execution Order

Imagine you have

Database Fixture

↓

Login Fixture

↓

Page Fixture

↓

Test

Execution

Database

↓

Login

↓

Page

↓

Test

↓

Page Cleanup

↓

Login Cleanup

↓

Database Cleanup

Notice cleanup happens in reverse order, like stacking plates.


Folder Structure

A common project structure is:

project

│

├── tests

│      login.spec.ts

│      dashboard.spec.ts

│

├── fixtures

│      baseFixture.ts

│      loginFixture.ts

│      apiFixture.ts

│

├── pages

│      LoginPage.ts

│      DashboardPage.ts

Best Practices

  • Keep fixtures focused on one responsibility (e.g., login, API client, database).
  • Avoid putting test assertions inside fixtures.
  • Reuse fixtures across multiple test files.
  • Use test fixtures for isolated resources and worker fixtures for expensive shared setup.
  • Clean up every resource you create (close pages, contexts, database connections, etc.).
  • Give fixtures meaningful names such as loggedInPage, adminUser, or apiClient.

Complete Flow Diagram

                Test Starts
                     │
                     ▼
         Playwright Reads Fixtures
                     │
                     ▼
         Execute Setup Code
   (Browser, Context, Page, Login)
                     │
                     ▼
             await use(resource)
                     │
                     ▼
          Test Receives Resource
                     │
                     ▼
          Test Executes Assertions
                     │
                     ▼
        Control Returns to Fixture
                     │
                     ▼
          Execute Cleanup Code
 (Close Page, Context, Database, etc.)
                     │
                     ▼
                 Test Ends

Leave a Comment