Answer
Playwright Fixtures
Fixtures are reusable setup/teardown functions that Playwright injects into tests. Built-in fixtures include page, browser, context, request, and browserName.
Built-in Fixtures
TypeScript
test('uses built-in fixtures', async ({
page, // A new Page in a new BrowserContext
browser, // The Browser instance
context, // The BrowserContext
request, // API request context
browserName // 'chromium' | 'firefox' | 'webkit'
}) => {
await page.goto('/');
});
Creating Custom Fixtures
Extend test with test.extend() to add your own fixtures:
TypeScript
// fixtures.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { ProductPage } from './pages/ProductPage';
// Define the fixture types
type MyFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
productPage: ProductPage;
authenticatedPage: LoginPage;
};
// Extend the base test
export const test = base.extend<MyFixtures>({
// Simple page object fixture
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage); // 'use' provides the fixture to the test
// Teardown happens after 'use' returns
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
productPage: async ({ page }, use) => {
await use(new ProductPage(page));
},
// Fixture that logs in before the test
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@test.com');
await page.getByLabel('Password').fill('pass123');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await use(new LoginPage(page)); // Test runs here
// Optional: logout after test
await page.getByRole('button', { name: 'Logout' }).click();
},
});
export { expect } from '@playwright/test';
Using Custom Fixtures in Tests
TypeScript
// tests/dashboard.spec.ts
import { test, expect } from '../fixtures';
test('dashboard loads with auth fixture', async ({ dashboardPage, authenticatedPage }) => {
// Already logged in — authenticatedPage fixture handled it
await expect(dashboardPage.welcomeHeading).toBeVisible();
});
test('login page has correct title', async ({ loginPage }) => {
await loginPage.navigate();
await expect(loginPage.heading).toHaveText('Sign In');
});
Fixture Scope — Worker-Level Fixtures
By default, fixtures run per test. Use scope: 'worker' for shared setup:
TypeScript
type WorkerFixtures = {
workerStorageState: string;
};
export const test = base.extend<{}, WorkerFixtures>({
workerStorageState: [async ({ browser }, use) => {
// Runs ONCE per worker — shared across tests in that worker
const page = await browser.newPage();
await page.goto('/login');
await page.fill('#email', 'admin@test.com');
await page.fill('#password', 'admin');
await page.click('#submit');
await page.context().storageState({ path: 'admin-auth.json' });
await page.close();
await use('admin-auth.json');
}, { scope: 'worker' }],
});
