QA How-To
Typing Playwright fixtures: A QA Guide (2026)
Learn typing Playwright fixtures QA teams can scale, including test and worker generics, options, dependencies, teardown, page objects, merging, and CI checks.
21 min read | 3,311 words
TL;DR
Type Playwright fixtures with test.extend<TestFixtures, WorkerFixtures>(), implement each fixture as an async function that awaits use(value), and let dependency injection define lifecycle order. Use worker scope only for safely shareable resources, options for configuration, and strict TypeScript checks to prevent missing or wrongly scoped fixtures.
Key Takeaways
- Pass test-scoped and worker-scoped fixture maps to test.extend as separate generic parameters.
- Put setup before await use(value) and teardown after it, because the use call is the lifecycle boundary.
- Model fixtures around owned resources and domain capabilities, not as an unstructured bag of helpers.
- Use option fixtures for project-specific values and auto fixtures only for behavior that truly applies to every test.
- Let fixture dependencies express setup and teardown order, and keep worker fixtures independent of test-scoped state.
- Export the extended test and its expect together, then type-check the suite separately from Playwright execution.
Typing Playwright fixtures qa engineers can trust means making resource shape, dependency, scope, and cleanup visible in TypeScript. The key API is test.extend<TestFixtures, WorkerFixtures>(). The first generic describes fixtures created for each test, while the second describes fixtures created once per worker process.
Good types do more than improve autocomplete. They prevent a test from requesting a fixture that does not exist, stop a worker resource from depending on test-only state, and document what setup gives to the test. This guide builds from a minimal custom fixture to worker resources, options, automatic behavior, merging, and practical failure diagnosis.
TL;DR
| Requirement | Playwright pattern | TypeScript shape |
|---|---|---|
| New object per test | default test-scoped fixture | first extend generic |
| Shared resource per worker | tuple with scope: 'worker' |
second extend generic |
| Project-configurable value | tuple with option: true |
include in option fixture map |
| Always run for every test | tuple with auto: true |
often type as void |
| Compose modules | mergeTests |
types are inferred from merged tests |
| Hide helper report noise | fixture option box: true |
no change to value type |
A fixture function receives dependencies, an async use callback, and test or worker information. Setup runs before await use(value), the test runs while that await is pending, and teardown runs after it resolves.
1. Typing Playwright fixtures QA fundamentals
Playwright Test includes fixtures such as page, context, browser, request, and browserName. A test asks for fixtures by destructuring its callback parameter:
import { test, expect } from '@playwright/test';
test('home page has a heading', async ({ page }) => {
await page.goto('https://playwright.dev/');
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
The runner inspects the fixture names, sets up only those required, injects typed values, and tears them down according to scope and dependency order. Custom fixtures use the same model.
A fixture is preferable to a broad beforeEach when a test needs a reusable value with clear ownership. It can depend on other fixtures, expose a domain object, and guarantee matching cleanup. Tests request only the capability they need.
Three ideas must remain distinct:
- A fixture type describes the value visible to tests.
- A fixture implementation creates, supplies, and disposes that value.
- Fixture options describe lifecycle metadata such as scope, automatic activation, or configurability.
Do not place implementation function types in the public fixture map. If tests receive an OrdersPage, type the field as OrdersPage, not as the async setup callback.
Playwright's injected parameter typing comes from the specific test object imported by the file. If a test imports the base test from @playwright/test instead of your extended export, custom fixtures will not exist. A consistent local import path is therefore part of the design.
Pair this guide with TypeScript config for tests QA so aliases, strictness, and Playwright's transform are checked in CI.
2. Create a minimal typed test fixture
A page object fixture is a clear starting point. Define a class with explicit Playwright types:
// tests/pages/todo-page.ts
import type { Locator, Page } from '@playwright/test';
export class TodoPage {
private readonly newTodo: Locator;
readonly items: Locator;
constructor(private readonly page: Page) {
this.newTodo = page.getByPlaceholder('What needs to be done?');
this.items = page.locator('.todo-list li');
}
async goto(): Promise<void> {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async add(text: string): Promise<void> {
await this.newTodo.fill(text);
await this.newTodo.press('Enter');
}
}
Extend the base test:
// tests/fixtures/test.ts
import { test as base, expect } from '@playwright/test';
import { TodoPage } from '../pages/todo-page';
type TestFixtures = {
todoPage: TodoPage;
};
export const test = base.extend<TestFixtures>({
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await use(todoPage);
}
});
export { expect };
Now import from the local fixture module:
// tests/todo.spec.ts
import { test, expect } from './fixtures/test';
test('adds an item', async ({ todoPage }) => {
await todoPage.add('Review fixture types');
await expect(todoPage.items).toHaveCount(1);
await expect(todoPage.items).toContainText('Review fixture types');
});
This example is runnable with Playwright Test after browsers are installed. Type inference knows todoPage is a TodoPage without annotations in the test callback.
Export expect beside test to give test authors one stable import. The standard Playwright expect works here, but a shared module also supports future custom matcher composition.
Avoid exporting the base test accidentally. Name it base and export only the extended test. If multiple feature teams extend it further, keep each layer small and document the intended import.
3. Understand the await use lifecycle boundary
The most important fixture line is await use(value). Everything before it is setup. The runner invokes the test while use is pending. When the test and dependent fixtures finish, use resolves and the code after it performs teardown.
A fully owned temporary directory fixture shows the pattern:
// tests/fixtures/temp.ts
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test as base } from '@playwright/test';
type TestFixtures = {
tempDir: string;
};
export const test = base.extend<TestFixtures>({
tempDir: async ({}, use, testInfo) => {
const prefix = join(tmpdir(), 'pw-' + testInfo.workerIndex + '-');
const directory = await mkdtemp(prefix);
try {
await use(directory);
} finally {
await rm(directory, { recursive: true, force: true });
}
}
});
The fixture value type is string because that is what the test receives. testInfo is inferred for a test-scoped fixture and supplies metadata such as worker index and output paths.
Always await use. Calling it without await can start teardown while the test is still accessing the resource. Returning before use also breaks lifecycle coordination.
Register resources only after successful creation. If mkdtemp rejects, no directory exists to clean. If setup performs several steps, clean partial resources in a setup catch or create them through nested owned fixtures.
Teardown should be idempotent where practical. The force: true removal tolerates a test that already removed its directory. For business data, a delete that receives "already absent" may also satisfy cleanup, but permission or transport failures should still surface.
A fixture should not contain test assertions about the product unless that assertion is part of setup validity. If login setup fails, a descriptive setup error is useful. Behavioral expectations belong in the test so reports identify the intended scenario.
The rules for preserving setup and cleanup failures are covered in promises and error handling in tests.
4. Type test-scoped and worker-scoped fixtures
Test-scoped fixtures are created for each test that requests them. Worker-scoped fixtures are created once for each worker process and shared by tests executed in that worker. Use the second generic type parameter for worker fixtures.
This runnable fixture creates one temporary directory per worker and one file path per test:
// tests/fixtures/files.ts
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test as base } from '@playwright/test';
type TestFixtures = {
artifactPath: string;
};
type WorkerFixtures = {
workerTempDir: string;
};
export const test = base.extend<TestFixtures, WorkerFixtures>({
workerTempDir: [
async ({}, use, workerInfo) => {
const prefix = join(tmpdir(), 'pw-worker-' + workerInfo.workerIndex + '-');
const directory = await mkdtemp(prefix);
try {
await use(directory);
} finally {
await rm(directory, { recursive: true, force: true });
}
},
{ scope: 'worker' }
],
artifactPath: async ({ workerTempDir }, use, testInfo) => {
const safeTitle = testInfo.title.replaceAll(/[^a-zA-Z0-9]+/g, '-');
await use(join(workerTempDir, safeTitle + '.json'));
}
});
The worker fixture uses workerInfo, while the test fixture receives testInfo. TypeScript derives the correct third argument from scope.
Scope is a cost and isolation decision. A browser process is expensive and safely managed by Playwright at worker scope. A mutable user session may be safer at test scope. Do not move a resource to worker scope only to make the suite faster. First prove that tests cannot change it in conflicting ways.
A worker-scoped fixture cannot depend on a test-scoped fixture because one worker resource outlives many test resources. Type checking and Playwright validation protect this direction. A test-scoped fixture may depend on a worker-scoped fixture, as artifactPath does.
With retries, a failed worker can be replaced, so worker-scoped setup may run again. Treat worker scope as "once per worker lifecycle," not "once for the entire suite." Global one-time environment setup is a different mechanism and should not be simulated with a mutable worker singleton.
5. Model fixture dependencies as a graph
Fixtures form a dependency graph. If ordersPage requests authenticatedPage, Playwright sets up the latter first. Teardown reverses the order, so ordersPage is disposed before the page or context it depends on.
import {
test as base,
expect,
type Page
} from '@playwright/test';
import { OrdersPage } from '../pages/orders-page';
type Fixtures = {
authenticatedPage: Page;
ordersPage: OrdersPage;
};
export const test = base.extend<Fixtures>({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByLabel('Email').fill('qa@example.test');
await page.getByLabel('Password').fill('not-a-real-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('banner')).toContainText('Signed in');
await use(page);
},
ordersPage: async ({ authenticatedPage }, use) => {
const ordersPage = new OrdersPage(authenticatedPage);
await ordersPage.goto();
await use(ordersPage);
}
});
The credentials above are illustrative fake data. Real suites should obtain controlled accounts through environment configuration or an account fixture, not hard-code secrets.
Keep dependencies narrow. A dashboard fixture that depends on database, email, queue, API, and three pages becomes expensive and hard to reuse. Prefer capability-focused fixtures so a read-only UI test does not initialize unrelated systems.
Circular dependencies are a design error. If fixture A needs B and B needs A, separate the shared primitive into fixture C or reconsider the ownership boundary.
Lazy activation is valuable. Nonautomatic fixtures run only when the requested test or hook needs them. A large fixture catalog is not inherently expensive if dependencies remain precise. Conversely, a beforeEach hook that requests a heavy fixture activates it for every test in that scope.
Use dependency names that describe the supplied capability, not the implementation. authenticatedPage is clearer than setup1. When a fixture replaces a built-in value, keep the standard name only if it preserves the expected contract.
6. Type domain clients and page objects as fixture values
Fixtures should expose useful domain capabilities rather than raw setup details. A typed API wrapper can own response checks while tests assert business behavior.
// tests/support/orders-api.ts
import type { APIRequestContext } from '@playwright/test';
export type Order = {
id: string;
status: 'created' | 'cancelled';
};
export class OrdersApi {
constructor(private readonly request: APIRequestContext) {}
async create(sku: string): Promise<Order> {
const response = await this.request.post('/api/orders', {
data: { sku, quantity: 1 }
});
if (!response.ok()) {
throw new Error('Create order failed with HTTP ' + response.status());
}
const body: unknown = await response.json();
if (!isOrder(body)) {
throw new TypeError('Create order response is not an Order');
}
return body;
}
async remove(id: string): Promise<void> {
const response = await this.request.delete('/api/orders/' + id);
if (!response.ok() && response.status() !== 404) {
throw new Error('Delete order failed with HTTP ' + response.status());
}
}
}
function isOrder(value: unknown): value is Order {
if (typeof value !== 'object' || value === null) {
return false;
}
const item = value as Record<string, unknown>;
return typeof item.id === 'string' &&
(item.status === 'created' || item.status === 'cancelled');
}
Provide it through the built-in request fixture:
import { test as base } from '@playwright/test';
import { OrdersApi } from '../support/orders-api';
type Fixtures = {
ordersApi: OrdersApi;
};
export const test = base.extend<Fixtures>({
ordersApi: async ({ request }, use) => {
await use(new OrdersApi(request));
}
});
The wrapper validates unknown JSON before returning Order. A cast alone would make the type look safe without checking runtime data.
Decide whether cleanup belongs in the fixture or test. If the fixture automatically creates one order, it should usually delete that order after use. If the fixture exposes a general client that can create many records, tests can register created IDs through a resource tracker fixture. Avoid a client that silently remembers mutable cleanup state across workers.
Page objects and clients should not extend the Playwright test object themselves. Keep values as normal classes or functions. The fixture module composes them, which makes the domain code easier to unit test.
7. Add typed option fixtures
Option fixtures let projects or test.use provide values through the same typed fixture system. They are useful for service URLs, personas, feature modes, and other values that vary by project or scope.
import { test as base } from '@playwright/test';
type Options = {
apiBaseUrl: string;
persona: 'buyer' | 'seller';
};
type Fixtures = {
authHeader: Record<string, string>;
};
export const test = base.extend<Fixtures & Options>({
apiBaseUrl: ['https://api.example.test', { option: true }],
persona: ['buyer', { option: true }],
authHeader: async ({ persona }, use) => {
const token = process.env[
persona === 'buyer' ? 'BUYER_TOKEN' : 'SELLER_TOKEN'
];
if (!token) {
throw new Error('Missing token for persona: ' + persona);
}
await use({ authorization: 'Bearer ' + token });
}
});
A test file can override an option:
import { test, expect } from './fixtures/test';
test.use({ persona: 'seller' });
test('uses the seller persona', async ({ persona, authHeader }) => {
expect(persona).toBe('seller');
expect(authHeader.authorization).toMatch(/^Bearer /);
});
A Playwright project can also set fixture options through its use configuration when the extended test consumes them. Keep secrets out of option defaults and configuration files. The option should select a secret source, while the secret remains in the execution environment.
Option types prevent invalid strings such as adminn from compiling. They also make supported modes discoverable. Use unions for small controlled sets and validated strings for environment-specific URLs.
Do not turn every constant into a fixture. Pure static data can remain a normal imported constant. An option fixture is valuable when it participates in test configuration, dependency injection, or project overrides.
If undefined is a meaningful option value, model it explicitly and understand how resetting a configured option behaves. Simpler nonnullable defaults are easier to operate.
8. Use automatic fixtures sparingly
An automatic fixture runs for every applicable test even if the callback does not name it. It is appropriate for cross-cutting behavior such as attaching safe debug context, enforcing a global cleanup check, or starting a per-test log collector.
import { test as base } from '@playwright/test';
type AutoFixtures = {
attachRuntime: void;
};
export const test = base.extend<AutoFixtures>({
attachRuntime: [
async ({}, use, testInfo) => {
await testInfo.attach('runtime.txt', {
body: Buffer.from(
'node=' + process.version + '\nworker=' + testInfo.workerIndex
),
contentType: 'text/plain'
});
await use();
},
{ auto: true, box: true }
]
});
The fixture is typed void because tests do not consume a value. auto: true activates it, and box: true keeps an uninteresting helper step from adding report noise. Boxing changes reporting, not execution or failure behavior.
Automatic fixtures can depend on other fixtures, which activates those dependencies for every test. Review that graph carefully. An auto fixture that requests page creates a page even for API-only cases. An auto fixture that requests an authenticated session can slow and couple the entire suite.
Prefer built-in configuration for capabilities it already covers, such as trace collection. A custom auto fixture should solve a repository-specific need and remain safe when setup fails or a retry starts.
Global beforeEach behavior can also be modeled as an auto fixture, but that does not mean all hooks should be rewritten. Use the form that best communicates dependencies and ownership. A fixture is especially strong when it supplies a value or needs symmetrical teardown.
Attach only safe metadata. Runtime versions and worker IDs are useful. Environment tokens, storage state, and full customer payloads are not.
9. Override built-in fixtures carefully
Playwright allows an extended test to override a built-in fixture such as page, context, or storageState. An override affects every consumer of that fixture, including custom dependencies, so preserve the built-in contract.
This override navigates each requested page to the configured base URL:
import { test as base } from '@playwright/test';
export const test = base.extend({
page: async ({ baseURL, page }, use) => {
if (!baseURL) {
throw new Error('baseURL is required by the page fixture');
}
await page.goto(baseURL);
await use(page);
}
});
The type is inferred from the base fixture. No custom generic entry is required because page already exists.
Use overrides only for behavior that is universally true for the extended test. Automatically navigating can surprise tests that verify redirects from a clean page or API-only cases that never need a page. A named homePage fixture may communicate intent better.
Overriding storageState can provide authentication state, but ensure each worker or test receives appropriately isolated credentials. A shared account can create race conditions even when browser contexts are isolated.
Do not manually close a built-in page, context, or browser in an override unless your override created a distinct resource and owns it. Playwright manages the built-in lifecycle. Double-closing can cause teardown noise and obscure the real failure.
When the override accepts baseURL, TypeScript knows the option can be undefined. Validate it rather than using a non-null assertion. That small guard converts a vague navigation error into a configuration failure.
10. Merge typed fixture modules safely
Large suites often keep database, accessibility, API, and UI fixtures in separate modules. Playwright exports mergeTests to combine extended test objects:
// tests/fixtures/index.ts
import { mergeTests } from '@playwright/test';
import { test as apiTest } from './api-test';
import { test as uiTest } from './ui-test';
export const test = mergeTests(apiTest, uiTest);
export { expect } from '@playwright/test';
Tests receive the merged fixture types:
import { test, expect } from './fixtures';
test('creates an order and shows it', async ({ ordersApi, ordersPage }) => {
const order = await ordersApi.create('BOOK-1');
await ordersPage.goto();
await expect(ordersPage.row(order.id)).toBeVisible();
});
Type inference saves you from manually intersecting maps. The real design concern is name collision. If both modules define account with different scope, type, or behavior, merging creates ambiguity or replacement risk. Use domain-specific names and one clear owner.
Merge shallow capability modules, not an unlimited graph of fixture bundles. Document which central test export is supported for general specs and which specialized exports exist for expensive environments.
Custom expect matchers can be composed separately with Playwright's matcher APIs when needed. Keep the imported expect aligned with the project's matcher extensions. A test that imports base expect may miss custom types and runtime matchers even if it imports the correct extended test.
Aliases can simplify the central import, for example @fixtures, but ensure Playwright and tsc resolve the same mapping. A relative import is safer than a misconfigured alias.
Before merging, run a small smoke test that requests one fixture from each module and fails deliberately inside the test. Confirm setup order, report names, and teardown for both capabilities.
11. Typing Playwright fixtures QA teams can scale
A scalable fixture catalog has small public types, explicit scopes, and minimal hidden work. Review fixture code as infrastructure because a defect affects many tests.
Use this checklist:
- The test file imports the intended extended
test, not the package base. - Test and worker fixture maps occupy the correct generic parameters.
- Every fixture awaits
useexactly once on the successful setup path. - Setup owns partial-failure cleanup, and teardown is safe after test failure.
- Worker fixtures depend only on worker-safe values.
- Mutable accounts, files, and records are isolated by test or worker.
- Option defaults contain no real secrets.
- Auto fixtures do not activate expensive unrelated dependencies.
- Built-in overrides preserve expected types and ownership.
- API response types are validated at runtime instead of asserted with casts.
- Merged modules use unique, domain-oriented fixture names.
- TypeScript checking runs separately from Playwright execution.
Keep fixture files focused. One module can define account allocation, another API clients, and another page objects. The central export composes them. Tests should not need to know setup order because dependencies encode it.
Avoid any in fixture maps. It spreads into every destructured test parameter and neutralizes the main benefit of typing. If a third-party client lacks types, isolate its unsafe boundary behind a narrow adapter and expose a typed capability.
Measure scope changes. Moving a fixture from test to worker scope may reduce setup calls but can introduce state leakage. Add tests that run in parallel and mutate the resource before approving the change.
Finally, make errors name the fixture boundary. "Could not allocate buyer account for worker 2" is more actionable than "beforeEach failed." Preserve the underlying cause and redact credentials.
12. Debug type and lifecycle failures
When TypeScript says a fixture property does not exist, check the import first. The spec may be using test from @playwright/test or another fixture module. Then confirm the field is in the correct generic map and the file is included in the intended tsconfig.
When Playwright reports an unknown parameter or scope dependency, inspect the fixture implementation graph. Common causes are a misspelled destructured name, a worker fixture requesting page, or a merge collision.
When teardown begins early, search for use(value) without await. When the process hangs, identify resources created by custom fixtures that never close. When tests pass alone but fail in parallel, inspect worker-scoped mutable data, shared account allocation, hard-coded files, and external service quotas.
A narrow diagnostic test helps:
import { test, expect } from './fixtures';
test('fixture smoke check', async ({
workerTempDir,
artifactPath
}, testInfo) => {
expect(artifactPath.startsWith(workerTempDir)).toBe(true);
expect(testInfo.workerIndex).toBeGreaterThanOrEqual(0);
});
Run the TypeScript compiler and Playwright separately:
npx tsc -p tsconfig.playwright.json --noEmit
npx playwright test tests/fixture-smoke.spec.ts --workers=2
Use more than one worker for isolation checks. A one-worker run cannot reveal every shared-state defect.
Trace Viewer and reports show fixture steps unless boxed. Keep important domain setup visible while boxing only low-value mechanics. A fixture title option can improve report language without changing its code name or type.
Do not fix a fixture failure with global retries first. Reproduce setup, test, and teardown paths deliberately. A retry creates a new worker in some failure situations and can make a scope defect appear intermittent.
Interview Questions and Answers
Q: What do the two generic parameters of test.extend represent?
The first map defines test-scoped custom fixture values, and the second defines worker-scoped custom fixture values. A worker fixture implementation also declares scope: 'worker'. Separating the maps lets TypeScript and Playwright enforce valid dependency lifetimes.
Q: What does await use(value) do?
It supplies the fixture value to the test and dependent fixtures. Setup code runs before that line, while teardown code runs after the awaited use resolves. Omitting await can start teardown before consumers finish.
Q: When should a fixture be worker-scoped?
Use worker scope for expensive resources that are safe to share among tests in one worker, such as a browser process or immutable service client. Do not use it for mutable sessions or records unless isolation is explicitly designed. A retry or worker restart can recreate it.
Q: Can a worker fixture depend on page?
No. page is test-scoped, so its lifetime is shorter than a worker fixture. A test fixture may depend on a worker fixture, but the inverse violates lifecycle ownership.
Q: What is an option fixture?
It is a typed fixture value marked with option: true that can be configured by a project or test.use. Options are suitable for personas, endpoints, and feature modes. Secret values should remain in environment storage rather than option defaults.
Q: Why use an automatic fixture?
An auto fixture applies cross-cutting setup or teardown to every applicable test without being named in the callback. Use it sparingly because its dependency graph also becomes automatic. It is useful for safe metadata, auditing, or repository-specific lifecycle enforcement.
Q: How do you combine fixture modules?
Use Playwright's mergeTests with the extended test objects, then export the merged test from one supported module. Types are inferred. Prevent name collisions and verify that the merged dependency graph remains understandable.
Q: How do typed fixtures improve parallel safety?
Types distinguish test and worker lifetimes and expose dependencies, which prevents some invalid sharing at compile or load time. They do not guarantee isolation by themselves. Resource allocation, unique data, cleanup, and service behavior must still be designed for concurrency.
Common Mistakes
- Importing
testfrom@playwright/testin a spec that needs custom fixtures. - Putting worker fixtures in the first generic map or forgetting
scope: 'worker'. - Calling
usewithout awaiting it. - Sharing mutable accounts or records at worker scope without isolation.
- Making an auto fixture depend on
pageand slowing API-only tests. - Hiding real credentials in fixture defaults or source code.
- Using
anyfor a fixture value and losing type safety across the suite. - Closing Playwright-owned built-in resources twice.
- Using a giant fixture that initializes unrelated services.
- Merging modules that define the same fixture name differently.
- Casting response JSON directly to a domain type.
- Assuming one worker fixture instance exists for the entire suite.
- Running Playwright without a separate TypeScript quality gate.
Correct fixture code gives every resource a clear owner and lifetime. Make scope and dependencies explicit before optimizing setup time.
Conclusion
Typing Playwright fixtures qa teams can scale starts with test.extend<TestFixtures, WorkerFixtures>() and a correct await use(value) boundary. Strong fixture types reveal capabilities, while scope and dependency design protect isolation and cleanup.
Create one small domain fixture, add a two-worker smoke test, and run both tsc --noEmit and Playwright. Once that lifecycle is trustworthy, compose options, automatic behavior, and merged modules only where they make test intent clearer.
Interview Questions and Answers
How are custom Playwright fixtures typed?
Define a map from fixture names to the values tests receive and pass it to test.extend. Use the first generic for test scope and the second for worker scope. The implementation is contextually typed from those maps and its metadata.
Explain fixture setup and teardown around use.
The fixture performs setup, then awaits use(value) while the test and dependent fixtures consume the value. After use resolves, teardown runs. This makes one fixture the owner of a resource's complete lifecycle.
Why can a worker fixture not depend on page?
Page is created per test, while a worker fixture spans multiple tests. The shorter-lived dependency cannot safely support the longer-lived resource. Put page-dependent behavior in a test-scoped fixture.
How do you decide fixture scope?
I compare setup cost with mutation and isolation risk. I use test scope for mutable sessions and records, and worker scope only for safely shareable resources. I also account for worker restarts and retries.
What are option fixtures used for?
They expose typed, project-configurable values through the fixture graph. Common examples are persona, endpoint, and feature mode. Options select behavior, while real secrets remain outside source control.
When would you use an auto fixture?
I use it for behavior that truly applies to every test, such as attaching safe runtime metadata or enforcing cleanup checks. I inspect its dependencies carefully because auto activation also activates them. I avoid using it as a hidden universal beforeEach.
How do you merge custom fixture sets?
Use mergeTests on the extended test objects and export one central merged test. Type inference combines their public fixtures. I prevent name collisions and run a smoke case that exercises setup and teardown from each module.
What does TypeScript not guarantee about fixtures?
It cannot prove external data is isolated, cleanup succeeds, credentials are safe, or a worker resource is semantically immutable. Types enforce shapes and some lifecycle relationships. Parallel safety still requires deliberate resource ownership and runtime checks.
Frequently Asked Questions
How do I type a custom Playwright fixture?
Create a type whose property maps the fixture name to the value tests receive, then pass it as the first generic to base.extend. Implement the fixture as an async function and await use with a value of that type.
How do I type a Playwright worker fixture?
Put the value in the second generic map of test.extend and define the implementation as a tuple with scope set to worker. Its dependencies must also be worker-safe.
Why is my custom fixture missing in a test?
The spec commonly imports the base test from @playwright/test instead of the extended local test. Also check the fixture name, generic map, merge export, and tsconfig file inclusion.
Should Playwright page objects be fixtures?
A page object is a good fixture value when it needs standard setup, dependencies, or navigation and is used across tests. A simple class can also be constructed directly when fixture lifecycle adds no value.
Can Playwright fixtures have teardown?
Yes. Put setup before await use(value) and teardown after it, usually inside try-finally for locally owned resources. Playwright waits for teardown according to reverse dependency order.
What is the difference between auto and option fixtures?
An auto fixture runs even when a test does not request it. An option fixture is a configurable typed value that projects or test.use can override. A fixture can have lifecycle metadata, but the two concepts solve different problems.
Does mergeTests preserve fixture types?
Yes. Playwright infers the fixtures supplied by the merged test objects. Teams still need unique names and compatible behavior to avoid semantic collisions.