skip to content

How do Playwright's built-in browser, context and page fixtures relate to each other in one test?

level: middleimportance: must knowfreq 70%

answer

  1. Two lifetimes, not three
  2. Worker holds the expensive one
  3. Context is the isolation boundary
  4. Page is a child of context
  5. browser.contexts() shows the nesting

basics

~20 s

They nest. The browser fixture is one browser shared by every test in a worker; the context fixture is a BrowserContext built fresh for the single test; the page fixture is the one page opened inside that context.

solid answer

~40 s

Three built-ins, two lifetimes. `browser` is **worker-scoped**: one browser instance serving every test that worker executes, because launching one is the expensive part. `context` is **test-scoped**: the runner asks that browser for a new `BrowserContext` before each test and disposes of it after. `page` is test-scoped too and is simply the first page opened in that context, which is why `page.context()` returns the `context` fixture object and `browser.contexts()` contains it while the test runs. Destructuring `{ browser, context, page }` in one test therefore gives you three views of the same nesting, not three independent things. The practical reading is that isolation lives at the context layer: shared browser means fast, fresh context means tests cannot reach each other's state.

code

typescript · 8 lines
typescript
import { test, expect } from '@playwright/test';

test('the built-ins nest', async ({ browser, context, page }) => {
  expect(context.browser()).toBe(browser);   // browser owns the context
  expect(page.context()).toBe(context);      // context owns the page
  expect(context.pages()[0]).toBe(page);
  expect(browser.contexts()).toContain(context);
});

go deeper

for a junior

Learn the order first: browser contains context, context contains page. Naming any of the three in a test gives you that object, not a copy of it.

for a middle

Explain the two lifetimes and why they differ, and be ready to show the identity checks that prove the nesting, such as page.context() matching the context fixture.

for a senior

Use the split when diagnosing: cross-test bleed almost never comes from the shared browser, so look at what the suite built above the fixtures instead.

for a principal

Frame it as a cost model. Decide what the team treats as shared infrastructure and what must be per test, and make the reasoning explicit rather than folklore.

## Two lifetimes, three fixtures Playwright Test's built-ins are not a flat bag of objects. They form a nesting with exactly two lifetimes in play: | Fixture | Scope | Rebuilt per test? | What it is | |---|---|---|---| | `browser` | worker | no | one running browser instance | | `context` | test | yes | a `BrowserContext` inside that browser | | `page` | test | yes | the single page opened in that context | | `request` | test | yes | an isolated `APIRequestContext` | | `browserName` | worker | no | the engine's name as a string | Read the table top to bottom and the design reads itself: the costly object is created once and shared, and everything a test must not share sits below it and is rebuilt. ## How one test sees the nesting Inside a test that destructures all three, these hold: - `page.context()` is the **same object** as the `context` fixture, not a copy; - `browser.contexts()` includes that context while the test is running; - `context.pages()[0]` is the `page` fixture; - `context.browser()` is the `browser` fixture. So the three names describe one chain: browser owns the context, context owns the page. ## Why the split is where it is 1. **Launching a browser is slow.** It is a process start plus engine warm-up, so paying it per test would dominate a suite's runtime. 2. **Creating a context is cheap.** It is an in-process construct, so a fresh one per test costs almost nothing. 3. **Isolation only needs the cheap layer.** Two tests in the same browser but different contexts do not see each other's session, so the shared browser costs you nothing in correctness. That is the whole argument, and it is what an interviewer is checking when they ask which of the three is shared. ## What it means for a suite Consider an internal admin console with several staff roles. Two tests in the same worker, one operating as an auditor and one as an administrator, run against the same browser process and against two different contexts. The auditor test cannot observe the administrator test's session, and the order they run in does not change either result. If you ever find that it does, the cause is something you introduced above the fixtures, not the fixtures themselves. Taking `browser` directly is legitimate but changes who owns cleanup. The fixture gives you the instance; anything you build from it, rather than receiving as a fixture, is yours to manage and does not inherit the per-test lifecycle the `context` and `page` fixtures have. ## Common misreadings - **"Each test gets its own browser."** No, and if it did, suites would be several times slower. - **"The context is per file."** No, it is per test. A file is not a lifetime in this model. - **"page and context are unrelated objects."** They are parent and child, and `page.context()` proves it at runtime. - **"Sharing the browser means sharing state."** State that matters to a test lives in the context, which is not shared. ## How to answer crisply Say the sentence in scope order, then the reason: "`browser` is worker-scoped and shared; `context` and `page` are test-scoped, so every test gets a new context with one page in it, and `page.context()` is that context. The expensive thing is shared, the cheap thing gives isolation." If the follow-up asks what happens with several workers, keep it simple: each worker owns its own browser, and each test inside it still gets its own context.

  • Which of the three would you name if a test needs to build something itself?
    `browser`, since it is the only one of the three that can create new contexts. Just remember that anything you create from it is outside the per-test fixture lifecycle, so its cleanup is your responsibility rather than the runner's.
  • Does sharing the browser across tests weaken isolation?
    No. The separation tests rely on lives at the `BrowserContext` layer, and each test gets its own. A shared browser process buys speed without letting one test observe another's session.
  • How does the request fixture fit into this nesting?
    It does not sit under the browser at all. `request` is a separate test-scoped fixture giving an isolated `APIRequestContext`, so a test can name it without a page or context ever being built.

saying these in an interview costs you the question

  • Calls the browser fixture test-scoped
  • Says the context is created once per file
  • Treats page and context as unrelated objects
  • Claims a shared browser leaks state between tests
  • Thinks context.browser() returns a new instance