Playwright Fixtures & Hooks
12 challenges covering fixture scope, teardown, auto-fixtures, storageState and extending Playwright's test model.
Challenge 1IntermediateScope
Solved
What is the difference between test and worker fixture scope?
You create a custom fixture. What is the key difference between
scope: 'test' and scope: 'worker'?const test = base.extend({
myDB: [async ({}, use) => {
const db = await createDB();
await use(db);
await db.close();
}, { scope: 'worker' }] // or scope: 'test'
});Answer: B — test = new per test; worker = shared per worker
scope: 'test' (the default): setup runs before each test, teardown runs after — completely isolated. scope: 'worker': setup runs once when the worker starts, shared across all tests in that worker, teardown runs when the worker exits. Use worker scope for expensive resources (DB connections, auth sessions) that are safe to share. Use test scope for anything that must be fresh per test (page state, data).Challenge 2Intermediateyield
Solved
What does yield separate in a fixture function?
In this fixture, what does the
yield keyword separate, and when does the code after yield run?const test = base.extend({
loginPage: async ({ page }, use) => {
const lp = new LoginPage(page);
await lp.goto(); // SETUP
await use(lp); // TEST RUNS HERE
await page.close(); // TEARDOWN — when does this run?
}
});Answer: B — teardown always runs (pass or fail)
In a fixture function, code before
In a fixture function, code before
await use() is the setup phase. The test executes during await use(value). Code after await use() is the teardown phase — it always runs, even if the test throws an error or assertion fails. This is guaranteed cleanup, similar to a try/finally pattern. This makes fixtures much safer than beforeEach/afterEach pairs where afterEach might not run if setup fails mid-way.Challenge 3BeginnerHooks
Solved
What is the scope of test.beforeAll()?
You place
test.beforeAll() inside a test.describe() block. How many times does it run?test.describe('Employee Tests', () => {
test.beforeAll(async () => {
console.log('beforeAll runs');
});
test('test one', async ({ page }) => { /* ... */ });
test('test two', async ({ page }) => { /* ... */ });
test('test three', async ({ page }) => { /* ... */ });
});Answer: B — once before the describe block's tests
test.beforeAll runs once before the first test in its containing describe block (or file if at the top level). test.beforeEach runs before every individual test. Nesting a beforeAll inside a describe scopes it to that describe block — it does not affect tests in sibling or parent describe blocks.Challenge 4Beginnerrequest fixture
Solved
What is the request built-in fixture?
What type does the built-in
request fixture provide, and what is it used for?test('create user via API', async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'Alice', role: 'tester' }
});
expect(response.status()).toBe(201);
});Answer: B — APIRequestContext without a browser
The built-in
The built-in
request fixture is an APIRequestContext instance — it makes HTTP requests directly without launching a browser. It supports all HTTP methods: request.get(), request.post(), request.put(), request.delete(), etc. It handles JSON serialization, cookies, and headers. Ideal for setting up test data via API before UI tests, or for pure API testing scenarios.Challenge 5AdvancedstorageState
Solved
What does storageState save and restore?
You use storageState to speed up authenticated tests. What does it capture?
// In globalSetup: login once and save
await page.context().storageState({ path: 'auth.json' });
// In tests: restore session
test.use({ storageState: 'auth.json' });Answer: B — cookies + localStorage for all origins
storageState saves cookies and localStorage entries from all origins into a JSON file. When restored via test.use({ storageState }), the browser context is pre-loaded with those values — so tests start already "logged in" without needing to go through the login UI every time. This is the standard Playwright pattern for authentication. Tab history, IndexedDB, and sessionStorage are not included.Challenge 6Intermediatetest.use()
Solved
What is the difference between test.use() and test.extend()?
When should you use
test.use() vs test.extend()?Answer: B — use() overrides locally; extend() adds/replaces fixture implementations
test.use({ storageState: 'auth.json' }) — overrides a built-in fixture option for the current test or describe scope. It's a scoped configuration override. test.extend({...}) — creates a new fixture function or replaces a built-in with a full custom implementation. extend returns a new test object; use mutates the scope's configuration inline.Challenge 7AdvancedglobalSetup
Solved
When does globalSetup run?
You configure
globalSetup: './global-setup.ts' in playwright.config.ts. When does it execute?Answer: C — once before all tests
globalSetup runs exactly once before the entire test suite starts. Use it for one-time operations: seeding a database, starting external services, generating auth tokens. The corresponding globalTeardown runs once after all tests finish. Values cannot be passed directly to tests — use environment variables or files (like auth.json) to share data from globalSetup to individual tests or fixtures.Challenge 8AdvancedDependency Chain
Solved
Can a fixture depend on another custom fixture?
Fixture B depends on fixture A. Does Playwright resolve this automatically?
const test = base.extend({
authToken: async ({}, use) => {
const token = await getAuthToken();
await use(token);
},
apiClient: async ({ authToken }, use) => {
const client = new APIClient(authToken); // depends on authToken
await use(client);
}
});Answer: B — Playwright resolves dependency chains automatically
Playwright's fixture engine performs dependency injection automatically. When
Playwright's fixture engine performs dependency injection automatically. When
apiClient declares authToken in its parameter list, Playwright ensures authToken is set up first, then passes it to apiClient. Chains can be multiple levels deep. This composability is a core strength of Playwright fixtures over manual beforeEach ordering.Challenge 9IntermediateWorker Scope
Solved
How many instances does a worker-scoped fixture create per parallel run?
You run Playwright with
workers: 3 and have a worker-scoped fixture. How many instances are created?Answer: B — one per worker process
Worker-scoped fixtures create exactly one instance per Playwright worker process. With 3 workers, you get 3 separate instances — each worker has its own copy. Tests within the same worker share that worker's instance. This makes worker-scoped fixtures good for expensive resources that can be safely shared within a worker but need isolation between parallel workers (e.g., separate DB connections).
Worker-scoped fixtures create exactly one instance per Playwright worker process. With 3 workers, you get 3 separate instances — each worker has its own copy. Tests within the same worker share that worker's instance. This makes worker-scoped fixtures good for expensive resources that can be safely shared within a worker but need isolation between parallel workers (e.g., separate DB connections).
Challenge 10BeginnerBuilt-in Fixtures
Solved
Can you use multiple built-in fixtures in one test?
Which of these is valid and correct in a single Playwright test?
test('multi-fixture test', async ({ page, request, browserName }) => {
// Using page, request AND browserName together
if (browserName === 'firefox') {
await request.get('/api/health');
}
await page.goto('/dashboard');
});Answer: B — multiple fixtures in one destructured object
Playwright injects fixtures via object destructuring. You can request any combination of built-in fixtures:
Playwright injects fixtures via object destructuring. You can request any combination of built-in fixtures:
page (browser page), request (API request context), browserName (current browser name), context (browser context), browser (browser instance), baseURL (config base URL), and more. All built-in fixtures are available simultaneously in any test.Challenge 11AdvancedOverride Built-ins
Solved
Can you override the built-in page fixture?
You want every test page to automatically start logged in. Is it possible to override the built-in
page fixture?const test = base.extend({
page: async ({ page }, use) => {
await page.goto('/login');
await page.fill('#username', 'admin');
await page.fill('#password', 'secret');
await page.click('[type="submit"]');
await use(page); // tests receive pre-logged-in page
}
});Answer: B — test.extend() can override any built-in
test.extend() lets you override any built-in Playwright fixture including page, context, and browser. In the override, you receive the original fixture as a parameter (the base page) and can customize it before calling await use(page). This is more maintainable than beforeEach because the override is composable, reusable across test files, and guaranteed to clean up.Challenge 12AdvancedAuto-fixtures
Solved
What does auto: true do for a fixture?
A fixture is defined with
{ auto: true }. How does this change its behaviour?const test = base.extend({
auditLogger: [async ({ page }, use) => {
await page.on('console', msg => log(msg));
await use();
}, { auto: true }] // ← auto
});Answer: B — runs for every test without needing to be declared
Auto-fixtures (
Auto-fixtures (
auto: true) are activated for every test in scope without the test function needing to request them as parameters. This is useful for cross-cutting concerns: logging, performance monitoring, screenshot on failure, accessibility auditing. The test code stays clean (no extra parameters), but the fixture runs automatically. Without auto: true, a fixture only runs when a test declares it.