Why fixtures enable clean Playwright projects.
// 1) import & alias
import { test as base } from '@playwright/test';
import { DashboardPage } from './poms/dashboard';
// 2) extend core with fixtures
export const test = base.extend({
dashboardPage: async ({ page }, use) => {
const pom = new DashboardPage(page, {
/* cfg later */
});
await use(pom); // expose to tests
}
});
export { expect } from '@playwright/test';
// BEFORE: init POM in every test
import { DashboardPage } from './poms/dashboard-page';
test('create check', async ({ page }) => {
const dash = new DashboardPage(page, { email: 'a', password: '...' });
await dash.login();
await dash.createCheck();
});
// AFTER: fixture injects ready-to-use POM
test('create check', async ({ dashboardPage }) => {
await dashboardPage.createCheck();
});
// base.ts
import { test as base } from '@playwright/test';
export const test = base.extend({
// tuple: [value, { option: true }]
user: [
{ email: 'stefan@checklyhq.com', password: '...' },
{ option: true } // mark configurable
],
dashboardPage: async ({ page }, use) => {
/* set later */
}
});
export { expect } from '@playwright/test';
// any.spec.ts
import { test, expect } from './base';
test('create check', async ({ dashboardPage, user }) => {
// `user` is available in tests
await expect(user.email).toBeTruthy();
await dashboardPage.createCheck();
});
// base.ts
import { test as base } from '@playwright/test';
import { DashboardPage } from './poms/dashboard';
export const test = base.extend({
user: [{ email: 'stefan@checklyhq.com', password: '...' }, { option: true }],
dashboardPage: async ({ page, user }, use) => {
const pom = new DashboardPage(page, user); // use option here
await pom.login();
await use(pom);
}
});
export { expect } from '@playwright/test';
use block// playwright.config.ts
import { defineConfig } from '@playwright/test';
import type { TestOptions } from './base'; // if you typed it
export default defineConfig<TestOptions>({
use: {
user: { email: 'me@company.com', password: '...' } // global default
}
});
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'user-a',
use: {
...devices['Desktop Chrome'],
user: { email: 'stefan@checklyhq.com', password: '...' } // per project
}
}
]
});
test.use// any.spec.ts
import { test } from './base';
// local override for this file/scope
test.use({ user: { email: 'hello@checklyhq.com', password: '' } });
test('create check (other user)', async ({ dashboardPage }) => {
await dashboardPage.createCheck();
});
flowchart LR
A[base.extend\nuser:{...}, option:true] --> B[Fixtures\nconsume option]
A --> C[Tests\nuse { user }]
A --> D[Config use{}\n(global)]
A --> E[Projects[].use{}\n(per project)]
A --> F[test.use{}\n(per test/spec)]
B --> G[Construct POM\nwith user]
C --> H[Run steps\nwith dashboardPage]
| Location | Scope |
|---|---|
base.extend |
default value |
config.use |
repo/global |
projects[].use |
per project run |
test.use |
per test/spec |
{ option: true } to make it overridable.use API everywhere for consistency.