Builds reusable Playwright fixtures via `test.extend` - picks the right scope (test vs worker), wires the `use(value)` setup/teardown split, composes auth (storageState per worker), database (per-test snapshot/restore), and feature-flag fixtures into one custom `test` object the whole suite imports. Outputs the `fixtures.ts` file plus per-fixture review notes (scope rationale, teardown ordering, `workerInfo.workerIndex` for parallel isolation). Use when the suite has copy-pasted `beforeEach` boilerplate that should be a fixture, or when adding auth / db / flag setup that crosses many specs.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
storageState per worker)Per pw-auth, the storageState pattern signs in once and reuses cookies + localStorage across tests:
// fixtures/auth.ts
import { test as base, type BrowserContext } from '@playwright/test';
import path from 'node:path';
type AuthFixtures = {
storageState: string;
};
export const test = base.extend<{}, AuthFixtures>({
storageState: [async ({ browser }, use, workerInfo) => {
const username = `user${workerInfo.workerIndex}`;
const fileName = path.resolve(`playwright/.auth/${username}.json`);
const page = await browser.newPage({ storageState: undefined });
await page.goto('/login');
await page.getByLabel('Email').fill(`${username}@example.com`);
await page.getByLabel('Password').fill('test-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: fileName });
await page.close();
await use(fileName);
}, { scope: 'worker' }],
});Per pw-auth, the storageState pattern uses
await page.context().storageState({ path: authFile }); to write
the cookie/localStorage snapshot, and tests pick it up via
storageState: 'playwright/.auth/user.json' on the test or project.
Per pw-fix, workerInfo.workerIndex is the canonical way
to derive per-worker unique values:
"A common use case is accessing
workerInfo.workerIndexto create unique resources per worker."
For per-worker accounts (when each worker mutates its own
server-side state), user${workerInfo.workerIndex} is the
canonical pattern.
test.use// tests/dashboard.spec.ts
import { test, expect } from './fixtures/auth';
test.use({ storageState: ({ storageState }, use) => use(storageState) });
test('shows the user dashboard', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.getByRole('heading')).toContainText('Welcome');
});Or set globally in playwright.config.ts:
projects: [
{
name: 'authenticated',
use: { storageState: 'playwright/.auth/user.json' },
},
],Page objects encapsulate per-page selectors and behaviors. The canonical example from pw-fix:
const test = base.extend<{ todoPage: TodoPage }>({
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await use(todoPage);
await todoPage.removeAll();
},
});The use() call demarcates setup vs teardown: code before is setup,
code after is teardown. The teardown runs even if the test fails.
// fixtures/db.ts
import { test as base } from './auth';
import { execSync } from 'node:child_process';
type DbFixtures = {
cleanDb: void;
};
export const test = base.extend<DbFixtures>({
cleanDb: [async ({}, use) => {
execSync('bash scripts/restore-test-db.sh', { stdio: 'inherit' });
await use();
// No teardown - next test runs `restore` itself.
}, { auto: true }],
});Per pw-fix, { auto: true } makes a fixture run for every
test even if the test doesn't list it in its parameters - the right
choice for cross-cutting state like DB reset.
The shell script delegates to a database snapshot/restore in restore mode.
// fixtures/flags.ts
import { test as base } from './db';
import { OpenFeature, InMemoryProvider } from '@openfeature/server-sdk';
type FlagFixtures = {
flags: (overrides: Record<string, unknown>) => Promise<void>;
};
export const test = base.extend<FlagFixtures>({
flags: async ({}, use) => {
const setFlags = async (overrides: Record<string, unknown>) => {
const provider = new InMemoryProvider(buildVariants(overrides));
await OpenFeature.setProviderAndWait(provider);
};
await use(setFlags);
// Teardown: restore an empty provider so the next test starts clean.
await OpenFeature.setProviderAndWait(new InMemoryProvider({}));
},
});
function buildVariants(overrides: Record<string, unknown>) {
return Object.fromEntries(
Object.entries(overrides).map(([k, v]) => [k, {
defaultVariant: 'configured',
variants: { configured: v },
disabled: false,
}]),
);
}A test that needs a flag picks the value:
test('shows new checkout when flag is on', async ({ page, flags }) => {
await flags({ new_checkout: true });
await page.goto('/checkout');
await expect(page.getByTestId('new-checkout-banner')).toBeVisible();
});For the matrix harness pattern (one shard per combo), see
feature-flag-test-harness.