Playwright Fixtures and Parameterized Testing |
This chapter introduces Playwrightβs Fixture mechanism, how to create custom Fixtures, and techniques for parameterized testing.
What is a Fixture?
A Fixture is Playwrightβs dependency injection mechanism for test function parameters.
Each test retrieves the required Fixture(s) via destructuring (e.g., { page }), and Playwright handles creation and cleanup automatically.
Example
// `page` and `context` are built-in Fixtures
test('Using built-in Fixtures', async ({ page, context }) => {
// Playwright automatically creates `page` and `context`
// Automatically cleans up after the test completes
await page.goto('');
});
Built-in Fixtures Reference
| Fixture | Type | Description |
|---|---|---|
page |
Page | An independent browser tab |
context |
BrowserContext | Browser context |
browser |
Browser | Browser instance |
browserName |
string | Name of the current browser |
request |
APIRequestContext | HTTP request client |
Custom Fixtures
Use test.extend() to create custom Fixtures, enabling injection of additional dependencies or overriding built-in Fixtures.
Example
// File path: tests/fixtures.ts
import { test as base, expect } from '@playwright/test';
// Define types for custom Fixtures
type MyFixtures = {
todoPage: { goto: () => Promise; addTodo: (text: string) => Promise };
authenticatedPage: any;
};
export const test = base.extend({
// Custom Fixture: Todo page object
todoPage: async ({ page }, use) => {
// Create
const todoPage = {
goto: async () => {
await page.goto('https://demo.playwright.dev/todomvc');
},
addTodo: async (text: string) => {
await page.getByPlaceholder('What needs to be done?').fill(text);
await page.keyboard.press('Enter');
},
};
// Inject into test
await use(todoPage);
// Cleanup logic (if needed)
},
// Custom Fixture: authenticated page
authenticatedPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
export { expect };
Using Custom Fixtures
Example
// File path: tests/todo.spec.ts
import { test, expect } from './fixtures';
test('Add Todo', async ({ todoPage }) => {
await todoPage.goto();
await todoPage.addTodo('Learn Playwright');
await todoPage.addTodo('Master Fixtures');
// Validation...
});
Fixture Scopes
Fixtures support three scopes, controlled via the scope option:
| Scope | Value | Lifecycle |
|---|---|---|
| Test-level (default) | scope: 'test' |
Created once per test |
| Worker-level | scope: 'worker' |
Created once per Worker process; shared across all tests within the same Worker |
Example
// Worker-level Fixture (shared across all tests within the same Worker)
const test = base.extend({
sharedResource: [
async ({}, use) => {
const resource = await createExpensiveResource();
await use(resource);
await resource.dispose();
},
{ scope: 'worker' } // Note: array syntax
],
});
Worker-level Fixtures are shared across multiple tests within the same Worker process, ideal for expensive resources like database connections.
Overriding Built-in Fixtures
You can override built-in Fixtures to change their default behavior.
Example
const test = base.extend({
// Override built-in `page` Fixture to change default viewport
page: async ({ baseURL, page }, use) => {
await page.setViewportSize({ width: 1920, height: 1080 });
await use(page);
},
// Override `storageState`
storageState: 'playwright/.auth/admin.json',
});
Fixture Auto-Cleanup
Fixture cleanup logic runs after the use() call, similar to afterEach behavior.
Example
const test = base.extend({
tempUser: async ({ request }, use) => {
// Create temporary user
const resp = await request.post('/api/users', {
data: { name: 'temp_' + Date.now() },
});
const user = await resp.json();
// Inject into test
await use(user);
// Auto cleanup: delete temporary user
await request.delete(`/api/users/${user.id}`);
},
});
Parameterized Testing
Playwright does not have a built-in parameterized testing mechanism, but tests can be dynamically generated using loops.
Example
// Data-driven testing
const testCases = [
{ username: 'admin', password: 'admin123', expected: 'Dashboard' },
{ username: 'user', password: 'user123', expected: 'Profile' },
{ username: 'guest', password: 'guest123', expected: 'Guest Page' },
];
for (const { username, password, expected } of testCases) {
test(`User ${username} sees ${expected} after login`, async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText(expected)).toBeVisible();
});
}
Test Annotations
Annotations add metadata to tests, which can be viewed and filtered in test reports.
Example
test('Test with annotations', async ({ page }) => {
// Add annotations (inside test function body)
test.info().annotations.push({
type: 'issue',
description: '
});
test.info().annotations.push({
type: 'description',
description: 'This test covers all branches of the login flow',
});
// Test logic...
});
// Using Tags (in test name)
test('Fast smoke test @smoke @critical', async ({ page }) => {
// Can run only this test via: npx playwright test --grep "@smoke"
});
YouTip