Playwright Masters

Playwright Fixtures: Complete Guide with Examples

If you are learning Playwright, you have probably already used a fixture without realizing it.

Look at this test:

import { test, expect } from ‘@playwright/test’;

 

test(‘homepage test’, async ({ page }) => {

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

 

  await expect(page).toHaveTitle(/Example/);

});

Table of Contents

Where did page come from?

You did not create a browser.

You did not create a browser context.

You did not create a page.

Playwright Test prepared the page fixture for you and supplied it to the test.

That is the basic idea behind Playwright Fixtures.

Fixtures become even more useful when your automation framework grows. Instead of repeating login steps, test-data creation, API setup, Page Object creation, or cleanup code in many tests, you can package that work into reusable fixtures.

This guide explains Playwright Fixtures from beginner concepts to practical framework design.

Playwright Fixtures

What Are Playwright Fixtures?

Playwright Fixtures are reusable pieces of test setup and cleanup that provide tests with the resources they need, such as pages, browser contexts, API requests, test data, or custom application objects.

Playwright Test prepares a fixture when a test needs it, gives the resulting value to the test, and manages its lifecycle.

In simple words:

A fixture is like a prepared toolbox that Playwright gives your test when the test needs something.

Playwright’s fixture system is a core part of Playwright Test. Built-in fixtures include resources such as page, context, browser, and request. Custom fixtures can be created with test.extend().

What Problem Do Playwright Fixtures Solve?

Imagine you have 100 tests.

Many of those tests need:

  • A browser page
  • A logged-in user
  • Test data
  • A Page Object
  • An API connection
  • Cleanup after the test

You could write the setup manually inside every test.

That quickly creates duplicate code.

For example:

test(‘test 1’, async ({ page }) => {

  await page.goto(‘/login’);

  // login

  // test

});

 

test(‘test 2’, async ({ page }) => {

  await page.goto(‘/login’);

  // login

  // test

});

 

test(‘test 3’, async ({ page }) => {

  await page.goto(‘/login’);

  // login

  // test

});

 

The test files become harder to maintain.

A fixture lets you move reusable setup into one place.

The relationship becomes:

Test

  ↓

Fixture

  ↓

Setup

  ↓

Resource

  ↓

Test runs

  ↓

Cleanup

 

This is one reason fixtures are useful in larger automation frameworks.

Why Are Playwright Fixtures Important?

Fixtures can help organize several types of test dependencies.

Test setup

A fixture can prepare a page or application state before a test.

Authentication

A fixture can provide a logged-in page or authentication state.

Page Objects

A fixture can create Page Object instances and give them to tests.

Test data

A fixture can prepare reusable data required by a test.

API testing

Playwright’s request fixture provides an API request context for tests.

Cleanup

A fixture can clean up resources after the test finishes.

Framework architecture

Fixtures can become the connection between your test files, Page Objects, API utilities, authentication, and test data.

The important idea is not to create fixtures for everything.

Create them when they provide a clear reusable dependency.

How Do Playwright Fixtures Work?

A fixture generally has four important stages:

Fixture requested

       ↓

Setup

       ↓

await use(value)

       ↓

Test runs

       ↓

Teardown / cleanup

 

The use() callback is especially important.

Consider:

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

  console.log(‘Setup’);

 

  await use();

 

  console.log(‘Cleanup’);

}

 

Everything before await use() is setup.

The value passed to use() becomes available to the test.

Code after await use() runs during teardown.

Playwright determines fixture dependencies and execution order. If fixture A depends on fixture B, B is set up before A and torn down after A. Non-automatic fixtures are also created only when required.

Built-in Playwright Fixtures

Playwright Test already provides several fixtures.

Some of the most commonly used are:

Fixture

What it provides

Common use

page

A Page

UI testing

context

A BrowserContext

Browser state and isolation

browser

Browser instance

Creating custom contexts

request

APIRequestContext

API testing

browserName

Current browser name

Browser-specific logic

The exact fixture API can evolve as Playwright adds capabilities, so the official fixture API should be used as the final reference.

The page Fixture

The page fixture is probably the fixture you will use most often.

test(‘login page’, async ({ page }) => {

  await page.goto(‘/login’);

 

  await expect(page.getByRole(‘heading’, {

    name: ‘Login’

  })).toBeVisible();

});

 

Playwright creates the page for the test.

You simply ask for it by putting page inside the test function’s first argument.

The context Fixture

A BrowserContext represents an isolated browser environment.

test(‘user test’, async ({ context }) => {

  const cookies = await context.cookies();

 

  console.log(cookies);

});

 

Contexts are important because Playwright uses them to isolate test environments. The default page fixture belongs to the test’s context.

The browser Fixture

The browser fixture gives access to the browser instance.

It is useful when you need to create additional contexts or pages manually for a particular scenario.

test(‘multiple users’, async ({ browser }) => {

  const userContext = await browser.newContext();

  const adminContext = await browser.newContext();

 

  const userPage = await userContext.newPage();

  const adminPage = await adminContext.newPage();

 

  // Test interaction between the two users.

 

  await userContext.close();

  await adminContext.close();

});

 

Do this only when the test actually needs multiple independent browser contexts.

The request Fixture

The request fixture is useful for API testing.

test(‘API test’, async ({ request }) => {

  const response = await request.get(‘/api/users’);

 

  expect(response.ok()).toBeTruthy();

});

 

Playwright provides an isolated API request context through this fixture.

For more API examples, see the Playwright API Testing guide.

Basic Playwright Fixture Example

  • Here is a normal Playwright test:

    import { test, expect } from ‘@playwright/test’;


    test(‘homepage test’, async ({ page }) => {

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


      await expect(page).toHaveTitle(/Example/);

    });


    Let’s understand it.

    test

    test(‘homepage test’, …)


    This defines a test.

    { page }

    async ({ page }) => {


    This asks Playwright for the built-in page fixture.

    The { page } syntax is JavaScript/TypeScript object destructuring.

    You are effectively saying:

    “Give me the page fixture because my test needs it.”

    Why don’t we create the page?

    Because Playwright Test manages the fixture.

    That is one of the main benefits of the fixture model.

What Is a Custom Playwright Fixture?

A custom fixture is a fixture that you create yourself.

For example, suppose many tests need a username.

You could create a custom fixture:

import { test as base } from ‘@playwright/test’;

type TestFixtures = {

  userName: string;

};

export const test = base.extend<TestFixtures>({

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

    await use(‘Rakesh’);

  },

});

Then:

test(‘user test’, async ({ userName }) => {

  console.log(userName);

});

The test receives:

userName

just like it receives:

page

The difference is that userName is your own fixture.

Understanding test.extend()

The foundation of custom Playwright Fixtures is:

test.extend()

Think about the original Playwright test as a basic toolbox.

Playwright test

      ↓

test.extend()

      ↓

Your custom test

      ↓

Built-in fixtures + custom fixtures

For example:

import { test as base } from ‘@playwright/test’;

type MyFixtures = {

  userName: string;

};

export const test = base.extend<MyFixtures>({

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

    await use(‘Rakesh’);

  },

});

Here:

  • base is the original Playwright test object.
  • MyFixtures describes your custom fixture types.
  • extend() creates a new test object.
  • userName is the custom fixture.
  • use() supplies the value to the test.

The official Playwright documentation uses test.extend() as the mechanism for creating custom fixtures.

Fixture Dependencies

Fixtures can depend on other fixtures.

This becomes powerful in real automation frameworks.

Imagine:

page

 ↓

login fixture

 ↓

dashboard page object

 ↓

test

The built-in page fixture can be used by the login fixture to perform authentication. 

For example:

import { test as base } from ‘@playwright/test’;

type MyFixtures = {

  loggedInPage: void;

};

export const test = base.extend<MyFixtures>({

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

    await page.goto(‘/login’);

    await page.getByLabel(‘Username’).fill(‘testuser’);

    await page.getByLabel(‘Password’).fill(‘password’);

    await page.getByRole(‘button’, { name: ‘Login’ }).click();

    await use();

  },

});

The custom fixture asks for:

{ page }

So Playwright prepares the page fixture first.

Then your fixture uses that page to perform login.

This is fixture composition.

Automatic Playwright Fixtures

Normally, a fixture is created when something requests it.

An automatic fixture is different.

You can define one using:

{ auto: true }

 

Example:

import { test as base } from ‘@playwright/test’;

 

export const test = base.extend({

  testLogger: [async ({}, use) => {

    console.log(‘Before test’);

 

    await use();

 

    console.log(‘After test’);

  }, { auto: true }],

});

 

Because it is automatic, Playwright starts it for the relevant test or worker even when the test does not explicitly list testLogger.

Automatic fixtures can be useful for cross-cutting behavior such as logging or collecting diagnostic information. They should be used carefully because they introduce behavior that is less visible in the test itself.

Playwright Fixture Scope

Fixture scope controls how long a fixture lives.

The two important scopes are:

Scope

Lifetime

Useful for

Test

Each test

Test-specific resources

Worker

One worker process

Expensive setup that can safely be shared within a worker

A test-scoped fixture is created for a test and cleaned up when that test finishes.

A worker-scoped fixture lives for the lifetime of the worker process.

Test Scope

Use test scope when isolation is important.

For example:

Test 1 → fixture

Test 2 → fixture

Test 3 → fixture

 

Each test gets its own fixture instance.

Worker Scope

Worker scope can reduce repeated expensive setup.

Worker

 ├── Test 1

 ├── Test 2

 ├── Test 3

 └── Test 4

 

But be careful.

A worker-scoped fixture can introduce shared state.

If one test changes something that another test expects to remain unchanged, you can create difficult-to-debug failures.

Playwright documents worker fixtures as fixtures that are torn down when the worker process is torn down, while test-scoped fixtures are torn down after each test.

Simple rule:

Use the smallest scope that safely solves your problem.

Playwright Fixture Lifecycle

Consider:

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

  console.log(‘Setup’);

 

  await use(‘Hello’);

 

  console.log(‘Cleanup’);

}

 

The sequence is:

  1. Playwright starts fixture
  2. “Setup” is printed
  3. “Hello” is supplied through use()
  4. Test runs
  5. Test finishes
  6. Code after use() runs
  7. Cleanup happens

 

The most important line is:

await use(‘Hello’);

 

The value passed to use() becomes the fixture value.

So the test can receive:

test(‘example’, async ({ myFixture }) => {

  console.log(myFixture);

});

 

and the output is:

Hello

 

The section following use() is where you typically perform fixture teardown and cleanup. 

Authentication Fixtures

Authentication is one of the most practical areas for fixtures.

Imagine 50 tests require a logged-in user.

You do not necessarily want every test to repeat:

Open login page

Enter username

Enter password

Click login

Wait for dashboard

 

Playwright supports reusable authentication state through storageState. The official authentication guidance also describes setup projects and worker-based authentication strategies for different application-state requirements.

A custom fixture can also provide an authenticated Page Object or authenticated test state.

For example:

type MyFixtures = {

  loggedInPage: Page;

};

 

export const test = base.extend<MyFixtures>({

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

    await page.goto(‘/login’);

 

    await page.getByLabel(‘Username’).fill(

      process.env.TEST_USERNAME!

    );

 

    await page.getByLabel(‘Password’).fill(

      process.env.TEST_PASSWORD!

    );

 

    await page.getByRole(‘button’, {

      name: ‘Login’

    }).click();

 

    await page.waitForURL(‘**/dashboard’);

 

    await use(page);

  },

});

 

In a real project, credentials should come from secure configuration or CI secrets rather than being hard-coded into the repository.

Fixtures with Page Object Model

Fixtures and Page Object Model can work very well together.

Think of their responsibilities like this:

Test

 ↓

Fixture

 ↓

Page Object

 ↓

Playwright Page

 

The Page Object describes how to interact with a page.

The fixture controls how that Page Object is prepared and provided to tests.

For example:

// pages/LoginPage.ts

 

import { Page } from ‘@playwright/test’;

 

export class LoginPage {

  constructor(private page: Page) {}

 

  async login(username: string, password: string) {

    await this.page.getByLabel(‘Username’).fill(username);

    await this.page.getByLabel(‘Password’).fill(password);

 

    await this.page.getByRole(‘button’, {

      name: ‘Login’

    }).click();

  }

}

 

Now create a fixture:

// fixtures/test-fixtures.ts

 

import { test as base } from ‘@playwright/test’;

import { LoginPage } from ‘../pages/LoginPage’;

 

type MyFixtures = {

  loginPage: LoginPage;

};

 

export const test = base.extend<MyFixtures>({

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

    await use(new LoginPage(page));

  },

});

 

export { expect } from ‘@playwright/test’;

 

Then the test becomes:

import { test, expect } from ‘../fixtures/test-fixtures’;

 

test(‘login test’, async ({ loginPage }) => {

  await loginPage.login(‘testuser’, ‘password’);

});

 

This pattern is useful when Page Objects are repeatedly created and you want tests to receive ready-to-use objects.

Playwright’s own fixture documentation demonstrates custom fixtures together with Page Object Model patterns.

For more information, see the Playwright Page Object Model guide.

Fixtures for Test Data

Fixtures can also provide test data.

For example:

type TestFixtures = {

  testUser: {

    name: string;

    role: string;

  };

};

 

export const test = base.extend<TestFixtures>({

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

    await use({

      name: ‘Test User’,

      role: ‘customer’,

    });

  },

});

 

Then:

test(‘customer test’, async ({ testUser }) => {

  console.log(testUser.name);

  console.log(testUser.role);

});

 

Keep fixtures focused on setup and reusable resources rather than holding all test data. 

If a large JSON file or data factory is easier to manage separately, use a dedicated test-data module.

A fixture should provide something that behaves like a test dependency, not simply become a storage box for every constant in your project.

Fixtures for API Testing

Playwright’s request fixture provides an API request context.

Example:

import { test, expect } from ‘@playwright/test’;

 

test(‘get users’, async ({ request }) => {

  const response = await request.get(‘/api/users’);

 

  expect(response.ok()).toBeTruthy();

});

 

API requests can be used alongside browser interactions in the same test flow. 

For example:

API fixture

    ↓

Create test data

    ↓

Browser fixture

    ↓

Open application

    ↓

Verify data

 

This can be useful when creating data through the UI would make a test unnecessarily slow.

Playwright also supports sharing authentication state between API and browser contexts, which can be useful for hybrid API/UI workflows.

Learn more in the Playwright API Testing guide.

Fixtures for Databases and External Services

Fixtures can also manage external resources such as:

  • Test records
  • Temporary files
  • Mock services
  • Test accounts
  • API-created data
  • Database test data

For example, conceptually:

Fixture starts

     ↓

Create test data

     ↓

Test uses data

     ↓

Test finishes

     ↓

Fixture cleans data

 

The important rule is cleanup.

If a fixture creates an external resource, think about how that resource will be removed or reset.

Avoid destructive operations against shared or production systems.

Playwright Fixtures vs Hooks

Fixtures and hooks are related, but they are not identical.

Feature

Fixtures

Hooks

Reusable dependency

Strong

Limited

Provides a value

Yes

Usually no

Setup

Yes

Yes

Cleanup

Yes

Yes

Dependency composition

Yes

Less direct

Scope control

Test/worker

Hook lifecycle

Good for reusable objects

Yes

Not usually

Good for simple local setup

Yes

Yes

A hook such as:

test.beforeEach(async ({ page }) => {

  await page.goto(‘/login’);

});

 

can be perfectly appropriate for setup local to a test file or describe block.

A fixture becomes attractive when the setup represents a reusable dependency.

For example:

test(‘test’, async ({ dashboardPage }) => {

  await dashboardPage.openReports();

});

 

The test directly receives what it needs.

Playwright also documents using automatic fixtures when you need global-style before/after behavior across test files.

Fixtures vs Helper Functions

A helper function is normally something you call.

await login(page);

 

In Playwright, a fixture is a resource that the framework prepares and provides for your test to use.

test(‘dashboard’, async ({ loggedInPage }) => {

  // Use loggedInPage

});

 

Use a helper when:

  • The operation is simple.
  • You want to call it explicitly.
  • It does not need fixture lifecycle management.
  • The operation does not represent a reusable test dependency.

Use a fixture when:

  • Setup and cleanup belong together.
  • The resource is required by many tests.
  • The object has a lifecycle.
  • The resource depends on other fixtures.
  • You want Playwright to manage when it is created.

Do not convert every helper into a fixture.

Fixtures and Test Isolation

Good Playwright tests should avoid depending on hidden state left by another test.

For example:

Test A creates user

       ↓

Test B expects user

 

This creates a dependency between tests.

If Test A fails, Test B may also fail.

A better design is:

Test A → prepares what it needs

Test B → prepares what it needs

 

Playwright’s built-in context and page fixtures are designed around test isolation. Each test gets an isolated environment rather than simply reusing the same browser state.

Fixtures should support that isolation, not secretly defeat it.

Fixtures and Parallel Testing

Parallel execution can make a test suite much faster.

But parallel testing makes shared state more important.

Suppose four workers run:

Worker 1 → Test A

Worker 2 → Test B

Worker 3 → Test C

Worker 4 → Test D

 

If all four tests modify the same account or database record, they can interfere with each other.

Fixture design should therefore consider:

  • Test isolation
  • Worker scope
  • Unique test data
  • Authentication accounts
  • External resources
  • Cleanup

Worker scope can be useful for expensive resources, but it should not be used simply because it reduces setup time.

Recommended Playwright Project Structure

A beginner-friendly project might look like:

playwright-project/

├── tests/

│   ├── login.spec.ts

│   └── dashboard.spec.ts

├── fixtures/

│   └── test-fixtures.ts

├── pages/

│   ├── LoginPage.ts

│   └── DashboardPage.ts

├── test-data/

│   └── users.ts

├── utils/

│   └── helpers.ts

├── playwright.config.ts

└── package.json

 

tests/

Contains test scenarios.

fixtures/

Contains custom fixture definitions.

pages/

Contains Page Objects.

test-data/

Contains reusable test data.

utils/

Contains general utilities that do not need fixture lifecycle management.

playwright.config.ts

Contains Playwright configuration.

There is no single mandatory project structure. A small project may need only a few folders, while a large framework may require more separation.

Practical Real-World Example: Login Fixture + Page Object

Let’s combine the concepts.

Step 1: Create the Page Object

// pages/LoginPage.ts

 

import { Page } from ‘@playwright/test’;

 

export class LoginPage {

  constructor(private page: Page) {}

 

  async login(username: string, password: string) {

    await this.page.goto(‘/login’);

 

    await this.page.getByLabel(‘Username’).fill(username);

    await this.page.getByLabel(‘Password’).fill(password);

 

    await this.page.getByRole(‘button’, {

      name: ‘Login’

    }).click();

 

    await this.page.waitForURL(‘**/dashboard’);

  }

}

Step 2: Create the Fixture

// fixtures/test-fixtures.ts

 

import { test as base } from ‘@playwright/test’;

import { LoginPage } from ‘../pages/LoginPage’;

 

type MyFixtures = {

  loginPage: LoginPage;

};

 

export const test = base.extend<MyFixtures>({

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

    const loginPage = new LoginPage(page);

 

    await use(loginPage);

  },

});

 

export { expect } from ‘@playwright/test’;

Step 3: Use the Fixture

// tests/login.spec.ts

 

import { test, expect } from ‘../fixtures/test-fixtures’;

 

test(‘user can log in’, async ({ loginPage, page }) => {

  await loginPage.login(

    process.env.TEST_USERNAME!,

    process.env.TEST_PASSWORD!

  );

 

  await expect(

    page.getByRole(‘heading’, { name: ‘Dashboard’ })

  ).toBeVisible();

});

 

Now the test does not need to know how the LoginPage object is created.

The responsibilities are separated:

Test

 ↓

Uses loginPage

 ↓

LoginPage

 ↓

Uses Playwright page

 

This is the real value of fixtures.

Common Playwright Fixture Mistakes

1. Creating a fixture for everything

Not every function needs to be a fixture.

Use fixtures when lifecycle, dependency injection, reuse, or setup/cleanup make sense.

2. Making fixtures too complicated

If one fixture performs login, creates users, calls five APIs, modifies configuration, and prepares the database, debugging becomes difficult.

Prefer smaller responsibilities.

3. Using worker scope unnecessarily

Worker scope can improve performance, but shared state can create test interference.

Use it only when the resource is safe to share within a worker.

4. Forgetting cleanup

If your fixture creates something, ask:

“Who removes it?”

Put cleanup after await use() where appropriate.

5. Hiding too much logic

A test should still be understandable.

This:

test(‘checkout’, async ({ checkoutEnvironment }) => {

  // …

});

 

may become confusing if checkoutEnvironment secretly performs 25 unrelated operations.

6. Depending on test order

Fixtures should help tests become independent, not create a hidden chain between them.

7. Sharing mutable state

Be careful when multiple tests can change the same object, account, file, or external resource.

Playwright Fixture Best Practices

Use these rules when designing fixtures:

  1. Keep fixtures focused.
  2. Give fixtures clear names.
  3. Keep setup and teardown together.
  4. Use the smallest safe scope.
  5. Avoid hidden side effects.
  6. Preserve test isolation.
  7. Do not create fixtures just because you can.
  8. Keep Page Objects responsible for page behavior.
  9. Keep test-specific expectations readable.
  10. Use secure configuration for credentials.
  11. Consider fixture performance in large suites.
  12. Document complicated fixtures.
  13. Prefer composition over one giant fixture.
  14. Make failures easy to understand.

A useful test should still tell another tester what is happening.

Troubleshooting Playwright Fixtures

Problem

Possible cause

What to check

Fixture is not recognized

Wrong import

Import your extended test

TypeScript error

Incorrect fixture type

Check the extend<…>() type

test.extend() problem

Incorrect fixture definition

Check fixture syntax

Cleanup is missing

Code not placed after use()

Review fixture lifecycle

Tests became slow

Expensive setup

Review scope and dependencies

Shared state failures

Worker/global resource

Check isolation

Authentication fails

Invalid/expired state

Check storageState and credentials

Fixture runs unexpectedly

Automatic fixture

Check { auto: true }

Fixture timeout

Slow setup/teardown

Review fixture timeout configuration

Playwright counts fixture setup and teardown as part of test execution time, and the documentation also supports configuring a separate fixture timeout for slow fixtures.

Playwright Fixtures: JavaScript vs TypeScript

Fixtures work with both JavaScript and TypeScript.

A JavaScript version can be simple:

const { test: base } = require(‘@playwright/test’);

 

exports.test = base.extend({

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

    await use(‘Rakesh’);

  },

});

 

TypeScript provides additional type information:

type MyFixtures = {

  userName: string;

};

 

export const test = base.extend<MyFixtures>({

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

    await use(‘Rakesh’);

  },

});

 

The practical difference is that TypeScript can help identify incorrect fixture names and incompatible values during development.

That does not mean JavaScript is wrong.

Choose the language that fits your project and team’s skills.

Playwright Fixtures in CI/CD

Fixtures become particularly important when tests run automatically in CI/CD.

A CI environment may run tests:

  • On every pull request
  • On every commit
  • In multiple browsers
  • In parallel workers
  • On different environments

Your fixtures should therefore avoid assumptions about a developer’s local machine.

Good CI-friendly fixtures should:

  • Read environment-specific configuration safely.
  • Avoid hard-coded credentials.
  • Create predictable test data.
  • Clean up temporary resources.
  • Avoid depending on previous tests.
  • Work correctly with parallel workers.
  • Produce useful debugging information.

Authentication state needs special care in CI because stored browser state can contain sensitive cookies or headers. Playwright recommends keeping authentication-state files out of source control.

Debugging Playwright Fixtures

Fixture failures can sometimes look like normal test failures.

Useful debugging tools include:

  • Playwright Inspector
  • Debug mode
  • Console logging
  • Screenshots
  • HTML reports
  • Trace Viewer

For example:

npx playwright test –debug

 

You can also use traces when investigating failures.

For CI, Playwright documents trace: ‘on-first-retry’ as a useful configuration option. Trace Viewer lets you inspect actions, DOM snapshots, console messages, network information, and other execution details.

A useful CI configuration is:

import { defineConfig } from ‘@playwright/test’;

 

export default defineConfig({

  retries: 1,

 

  use: {

    trace: ‘on-first-retry’,

  },

});

 

This gives you diagnostic information when a test needs a retry without recording a full trace for every successful test.

Playwright Fixtures Compared With Selenium and Cypress Approaches

Different automation frameworks organize test setup differently.

Area

Playwright

Selenium

Cypress

Test setup model

Fixtures + hooks

Often framework/test-runner dependent

Hooks and Cypress-specific mechanisms

Dependency injection

Built into Playwright Test fixtures

Depends on framework

Different model

Browser context isolation

Core Playwright concept

Usually managed through driver/session patterns

Cypress manages browser/test state differently

Custom setup objects

Custom fixtures

Commonly helpers/base classes/framework utilities

Commands, helpers, hooks

Authentication

Storage state and other patterns

Commonly cookies/session/framework utilities

Session mechanisms and commands

API integration

request fixture/APIRequestContext

Depends on libraries

cy.request()

Fixture lifecycle

Explicit setup/teardown through use()

Depends on implementation

Different lifecycle model

The important point is not that one framework is universally better.

The important point is understanding Playwright’s model.

In Playwright Test, fixtures are a first-class part of the test runner, so reusable dependencies can be requested directly by tests.

How to Learn Playwright Fixtures

If fixtures seem complicated at first, learn them in this order:

Step 1: Learn basic Playwright tests

Understand:

test()

page

goto()

click()

fill()

expect()

 

Step 2: Learn locators

Understand how Playwright identifies page elements.

See the Playwright Locators guide.

Step 3: Learn assertions

Understand how tests verify results.

See the Playwright Assertions guide.

Step 4: Learn hooks

Understand beforeEach, afterEach, beforeAll, and afterAll.

Step 5: Learn Page Object Model

Understand how reusable page behavior is organized.

Step 6: Learn built-in fixtures

Start with:

page

context

browser

request

 

Step 7: Learn test.extend()

Create one very small custom fixture.

Step 8: Learn dependencies

Create a fixture that depends on page.

Step 9: Learn lifecycle

Understand:

Setup

 ↓

use()

 ↓

Test

 ↓

Cleanup

 

Step 10: Learn scope

Understand test-scoped versus worker-scoped fixtures.

Step 11: Build an authentication fixture

Apply the concepts to a realistic application.

Step 12: Combine fixtures with Page Objects

Now you are starting to build framework architecture.

Step 13: Add API and CI/CD concepts

Finally, connect fixtures to API setup, test data, parallel workers, authentication, and CI.

The PlaywrightMasters roadmap also places fixtures within the broader progression from Playwright fundamentals toward framework development, API testing, CI/CD, and real projects.

How Playwright Fixtures Help With Real-World Automation

Knowing page.goto() and page.click() is only part of automation testing.

Real projects also require you to think about:

Test Design

    ↓

Reusable Setup

    ↓

Authentication

    ↓

Page Objects

    ↓

Test Data

    ↓

API Integration

    ↓

Test Isolation

    ↓

Parallel Execution

    ↓

CI/CD

    ↓

Debugging

 

Fixtures can connect many of these pieces.

For example:

Test

 ↓

Dashboard Fixture

 ↓

Authenticated Page

 ↓

Browser Context

 ↓

Application

 

Or:

Test

 ↓

Test Data Fixture

 ↓

API Request

 ↓

Created Data

 ↓

Browser Test

 

This is why fixtures are an important framework-design concept for automation testers.

They are not simply another Playwright command to memorize.

They teach you how to organize dependencies around tests.

Frequently Asked Questions About Playwright Fixtures

 How do Playwright fixtures make test setup easier?

Playwright fixtures move common test preparation into reusable components. Instead of repeating actions such as opening pages, preparing users, creating test data, or configuring dependencies in every test, you can define that preparation once and request it wherever needed.

 

 How does a test receive a fixture in Playwright?

 

A test receives a fixture through the arguments of the test function. Playwright Test identifies the requested fixture, prepares it according to its dependencies and scope, and then makes the resulting value available to the test.

 What can you build with a custom Playwright fixture?

 

A custom fixture can represent almost any reusable resource needed by an automation project. For example, you can create fixtures for logged-in users, Page Objects, API clients, test accounts, prepared application data, or environment-specific resources.

 Why is `await use()` important in a Playwright fixture?

 

`await use()` marks the point where the fixture hands its prepared value to the test. Once the test finishes using that value, Playwright continues with the code written after `await use()`, making that section suitable for teardown or cleanup.

 Can Playwright fixtures prepare different application states?

 

Yes. Fixtures can prepare different states before a test begins. For example, separate fixtures can provide an administrator session, a customer session, or an unauthenticated browser state, allowing tests to start from the condition they actually need.

 How can fixtures reduce repeated login code?

 

A fixture can centralize authentication preparation so individual tests do not have to repeat the same login workflow. Depending on the application, the fixture can work with an existing authentication state, create the required session, and provide the authenticated environment to the test.

 

 How do fixtures work with Playwright’s dependency system?

 

A fixture can request another fixture as an input, creating a dependency chain. For example, a custom Page Object fixture can receive `page`, while another fixture can depend on that Page Object. Playwright then prepares the required dependencies in the appropriate order.

 

 When should I use a fixture instead of a utility function?

 

Use a fixture when the resource needs Playwright Test to control its preparation, availability, dependencies, or cleanup. A utility function is generally better for a standalone operation that simply receives arguments, performs an action, and returns a result without needing fixture lifecycle management.

 How can Playwright fixtures support reliable test isolation?

 

Fixtures can create fresh resources or controlled application states for tests instead of relying on state left behind by previous tests. Choosing the correct fixture scope and avoiding unnecessary shared mutable data can help prevent one test from affecting another.

 What is a practical real-world use case for Playwright fixtures?

 

A common real-world pattern is a test that needs an authenticated customer, an application page, and prepared data. A fixture can coordinate these requirements before the test starts, provide the required objects to the test, and handle cleanup afterward. This allows the test itself to concentrate on the behavior being verified.

Conclusion

Playwright Fixtures are one of the most important concepts to understand when moving from simple Playwright scripts to maintainable automation frameworks.

Start with the built-in fixtures:

page

context

browser

request

 

Then learn:

test.extend()

 ↓

Custom Fixtures

 ↓

Dependencies

 ↓

Lifecycle

 ↓

Scope

 ↓

Authentication

 ↓

Page Objects

 ↓

API Testing

 ↓

CI/CD

 

The goal is not to create as many fixtures as possible.

The goal is to create small, understandable, reusable dependencies that make your tests easier to write and maintain.

If you are a beginner, start with one custom fixture.

Create it.

Use it in one test.

Understand use().

Then learn fixture dependencies and scope.

That gradual approach will make the Playwright fixture system much easier to understand.

Playwright Masters automation testing logo - White Back ground

Playwright Masters Team

Playwright Automation Testing Experts | Industry-Focused Training & Practical Learning

Playwright Masters is a dedicated automation testing training platform focused on helping learners build practical skills in Playwright automation testing. Our training covers real-world automation concepts, framework practices, coding, debugging, API testing, cross-browser testing, CI/CD, and interview preparation to help learners develop job-ready testing skills.

Scroll to Top