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
| Fixture | Purpose |
|---|---|
| browser | Browser instance |
| page | New page |
| context | Browser context |
| request | API testing |
| browserName | Browser 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, orapiClient.
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