Introduction
If you've spent any time working with Playwright, you've likely seen the test('my test', async ({ page }) => { ... }) syntax. But have you ever wondered where that page object comes from? In Playwright, page is a Fixture.
Fixtures are a powerful alternative to traditional beforeEach/afterEach hooks. They allow you to define a piece of setup logic that is only executed when a test explicitly requests it. This approach, rooted in dependency injection, makes your tests faster, more modular, and significantly cleaner. In this guide, we'll break down how fixtures work and how to create your own.
What is a Playwright Fixture?
Think of a fixture as a reusable, isolated environment setup. Instead of manually setting up a database or a logged-in state in Every. Single. Test. File. using hooks, you define a fixture once and "inject" it into your tests.
Key Benefits over Hooks:
- Lazy Loading: Fixtures are only created if they are used. If a test doesn't ask for a fixture, it isn't set up.
- Encapsulation: Setup and teardown for a specific resource are grouped together in one place.
- Speed: Playwright can optimize the execution of fixtures across multiple tests.
How to Create a Custom Fixture
Let's say we want to create a loggedIntUser fixture that automatically logs in before a test starts.
1. Defining the Fixture
First, create a new file (e.g., fixtures.ts) to extend the base Playwright test object.
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
type MyFixtures = {
adminPage: LoginPage;
};
export const test = base.extend<MyFixtures>({
adminPage: async ({ page }, use) => {
// SETUP: Log in as admin
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('admin@example.com', 'secret');
// USE the fixture in the test
await use(loginPage);
// TEARDOWN: Clear session (automatically happens after test)
await page.context().clearCookies();
},
});
export { expect } from '@playwright/test';
2. Using the Fixture in a Test
Now, instead of importing test from @playwright/test, import it from your custom fixtures file.
import { test, expect } from './fixtures';
test('Should see dashboard after admin login', async ({ adminPage }) => {
// adminPage is already logged in and ready to go!
await expect(adminPage.dashboardHeader).toBeVisible();
});
Advanced: Fixture Scopes
Fixtures can have different scopes depending on how often they should be recreated:
testscope (default): Recreated for every test. Perfect for user sessions and UI state.workerscope: Created once per worker process. Perfect for heavy operations like starting a local database or a development server.
export const test = base.extend<{}, { database: string }>({
database: [async ({}, use) => {
// This runs once per worker
const db = await initDatabase();
await use(db);
await db.close();
}, { scope: 'worker' }],
});
Passing Data Between Fixtures
Fixtures can depend on other fixtures! This allows you to build a hierarchy of dependencies. For example, a cartPage fixture might depend on the browser and page fixtures.
export const test = base.extend({
authenticatedPage: async ({ page }, use) => {
await login(page);
await use(page);
},
checkoutPage: async ({ authenticatedPage }, use) => {
// Starts with a page that is already logged in
const checkout = new Checkout(authenticatedPage);
await use(checkout);
},
});
Why You Should Stop Using beforeEach
While beforeEach is familiar, it often leads to "Fat Test Files" where every test in the file is forced into the same setup, even if it doesn't need it. Custom fixtures allow you to keep your tests lean. If a test doesn't need a logged-in user, it just doesn't include { authenticatedPage } in its arguments.
Conclusion
Mastering Playwright Fixtures is what separates a beginner from a senior automation architect. They promote the "Don't Repeat Yourself" (DRY) principle and lead to a more maintainable, scalable testing framework. As we move into 2026, the reliance on advanced dependency injection patterns like this will only increase.
Frequently Asked Questions
Yes, but it's generally better to stick to one pattern to avoid confusion about the execution order.




