Boost your Playwright framework with Fixtures
- Kaviya Ramalingam
- Jan 17
- 4 min read
Updated: Jan 18
When working with Playwright , one of the most powerful features you will come across is fixtures.
Honestly ,when I first saw them , they just looked like another set up thing just similar to beforeEach.
But once my framework started growing , I realized fixtures are way more than that. They completely change how I structure page objects and tests.
In this blog, I will explain
What are fixtures in playwright?
How do built-in fixtures work?
Why are custom fixtures needed?
All the Examples I am sharing are from my real usage, not just theory.
What are fixtures?
Fixtures are kind of function that can run before each test and after each test.
They are like helpers that prepare the environment and provide commonly needed objects to every test in a controlled way.
Think of it like cooking. When you walk into the kitchen , you will not build the stove , connect the gas ,or install the sink. Those things are already there; you will just start cooking.
Similarly , in playwright, fixtures take care of the setup ,so the test can focus on implementing the test scenario.
test('Go To Url',async({page})=>{
await page.goto('https://tutorialsninja.com/demo/');
});The page object isn’t created by us , Playwright creates it for each test using built-in fixture.
Built-in Fixtures :
1. page:
 Represent a single browser tab.
Automatically created for each test.
Use it to navigate, type, click or interact with your app.
Page is your cooking stove, it’s ready to use, you just need to start cooking.
2. testInfo:
It contains information about currently running tests.
It gives information like test title , line , status , retries , tags , and output folder.
It’s useful for logging , screenshots on test failure and test reporting
const { test } = require('@playwright/test');
test('go to url', async ({ page }, testInfo) => {
await page.goto('https://tutorialsninja.com/demo/');
console.log('Test title:', testInfo.title);
console.log('Retry count:', testInfo.retry);
});It’s like a kitchen planner book, noting which recipes you tried, whether it worked, and how many attempts it took.
3.Browser :
It represents the entire browser instance.
It can be used to create multiple pages or contexts manually.
Browser fixture is rarely used because the page usually does the job.
test('open multiple pages',async({browser}) =>{
const context = await browser.newContext();
const pageOne = await context.newPage();
const pageTwo = await context.newPage();
await pageOne.goto('https://www.amazon.com');
await pageTwo.goto('https://www.flipkart.com');
});4.context
A browser context is like a fresh browser profile.
cookies, local storage, and sessions are isolated per context
If page fixture is used, Playwright already creates a fresh context per test.
test('isolated session', async ({ context }) => {
const page = await context.newPage();
await page.goto('https://tutorialsninja.com/demo/');
});Tip: If you rely on page , you don’t need to touch the context or the browser directly, unless the scenario specifically needs multiple pages or isolated sessions.
Custom Fixtures: Making Your Framework Smarter
Built-in fixtures like page and testInfo provide a strong foundation. But if you are working on larger projects, you often need reusable components to work across all tests.
The kitchen has a stove, sink ,and oven ready (Built-in fixtures) , but you also want all your cooking tools(knives, pans, spatulas ) ready before you start. That’s what custom fixture does for tests.
Example:
Before Adding custom Fixture:
Without a fixture , page objects need to be initialized for each and every tests .
import { test } from '@playwright/test';
import { HomePage } from '../Pages/HomePage.js';
import { ProductPage } from '../Pages/ProductPage.js';
let home, product;
test.beforeEach(async ({ page }) => {
home= new HomePage(page);
product = new ProductPage(page);
await page.goto('https://tutorialsninja.com/demo/');
});
test ('place a order', async ({ page }) => {
await home.selectEuroCurrency();
await home.addCanonToCart();
await product.clickAddToCart();
await product.verifyErrorMessage();
});Drawbacks:
setup is repeated in every test file.
Its harder to maintain if page objects keeps on adding.
Tests are longer and less readable.
How custom fixtures work:
A custom fixtures in playwright is built using : extend and use
base.extend({…}) is used to create a new fixture on top of the Playwright built in test. This is where we define what needs to be built for the test .
Use({ …}) is used to pass the prepared values to the test. Whatever is provided to use will be available inside the test.
import { HomePage } from '../Pages/HomePage.js';
import { ProductPage } from '../Pages/ProductPage.js';
export const test = base.extend({
pages:async({ page }, use)=> {
const home = new HomePage(page);
const product = new ProductPage(page);
await use ({ home, product });
}
});This fixture creates all page objects once per test. Tests can use these objects without worrying about the page or setup.
After adding Fixture:
import { test } from '../Fixtures/PagesFixture.js';
test.only('Place a order', async ({ pages }) => {
await pages.home.goToUrl();
await pages.home.selectEuroCurrency();
await pages.home.addCanonToCart();
await pages.product.clickAddToCart();
await pages.product.verifyErrorMessage();
});Benefits:
No need of repeated page initialization which makes the test cleaner.
can add more page objects in the fixture, and they can be used by all tests.
If there is any change, only the fixtures need to be updated without changing the test .
Fixture can be customized-we can add testdata, retries to it.
For example, the existing pages fixture can now be customized by adding test data , so everything that a tests needs, come from a single place.
import { test as base } from '@playwright/test';
import { HomePage } from '../Pages/HomePage.js';
import { ProductPage } from '../Pages/ProductPage.js';
export const test = base.extend({
pages: async ({ page }, use) => {
const home = new HomePage(page);
const product = new ProductPage(page);
await use({ home, product});
},
testData: async ({}, use) => {
const data = {
currency: 'Euro',
productName: 'Canon EOS 5D'
};
await use(data);
}
});import { test } from '../Fixtures/PagesFixture.js';
test.only('Place a simple order', async ({ pages,testData }) => {
await pages.home.goToUrl();
await pages.home.selectEuroCurrency(testData.currency);
await pages.home.addCanonToCart(testData.productName);
await pages.product.clickAddToCart();
await pages.product.verifyErrorMessage();
});Here, instead of hardcoding the data inside the tests or spreading it across multiple test files, we move them into fixtures which become the single source for test data.
Conclusion:
At first , Fixtures feel like another reusable utility, but when the framework starts growing , they become the backbone that keeps everything optimized and readable
Built-in Fixtures such as page , testInfo eliminate repetitive setup and ensures that every test runs in a clean ,independent environment. Building the custom fixtures on top of it allows to design reusable test blocks such as page objects, test data — making the framework easier to maintain.
Happy Learning with Playwright fixtures!..