skip to content

A Playwright suite opens one page in test.beforeAll and reuses it across tests. What breaks?

level: seniorimportance: should knowfreq 55%

answer

  1. Fixture lifetime swapped for file lifetime
  2. Passes alone, fails together
  3. Order becomes load-bearing
  4. Re-runs start from dirty state
  5. Cleanup is now yours to write

basics

~20 s

Test independence. Every test now drives the same page in the same context, so state accumulates, results depend on run order, and cleanup becomes the author's problem instead of the runner's. The per-test guarantee of the page fixture is gone.

solid answer

~50 s

The suite has replaced a **test-scoped fixture with a hand-managed object**, and it loses everything the fixture was providing. All tests share one context, so whatever the first test leaves behind, a signed-in session, an open dialog, a scrolled position, is the second test's starting state; failures then depend on order and on which subset you run. Running a single test on its own passes while the file fails, or the reverse, which is the classic signature. Nobody rebuilds that page between attempts, so a re-run of a failed test starts from the corrupted state rather than a clean one. Lifecycle is now yours: the page must be closed in an `afterAll`, and if a hook throws before that runs you leak it. The fix is to take `page` per test and move any shared cost into setup that is genuinely reusable, not into a shared live page.

code

typescript · 15 lines
typescript
import { test, expect, type Page } from '@playwright/test';

// Anti-pattern: one page, file lifetime, shared by every test below.
let shared: Page;
test.beforeAll(async ({ browser }) => {
  shared = await browser.newPage();               // you now own its close()
  await shared.goto('https://admin.example.com/staff');
});
test.afterAll(async () => await shared.close());

// Fix: take the fixture, get a fresh context per test.
test('an auditor sees the read-only view', async ({ page }) => {
  await page.goto('https://admin.example.com/staff');
  await expect(page.getByRole('button', { name: 'Edit' })).toBeHidden();
});

go deeper

for a junior

Recognise the smell: if tests only pass in one order, something is shared. Take the page fixture in each test rather than keeping a page in a file-level variable.

for a middle

Explain why beforeAll cannot receive page at all, and what changes when a page is created from the worker-scoped browser instead: lifetime, cleanup and shared context.

for a senior

Diagnose it from symptoms alone, run alone versus in file, reorder, re-run, and then argue the correct remedy so the team keeps its speed without giving up isolation.

for a principal

Set the boundary for the organisation: isolation is not negotiable, speed comes from cheaper setup, and reviews reject shared live state before it becomes the suite's architecture.

## What the code is really doing `test.beforeAll` cannot receive test-scoped fixtures, so the pattern always looks the same: reach for the worker-scoped `browser`, call `browser.newPage()`, keep the result in a file-level variable, and use it from every test. ```typescript let shared: Page; test.beforeAll(async ({ browser }) => { shared = await browser.newPage(); }); test.afterAll(async () => { await shared.close(); }); ``` That is not a small style deviation. It swaps a runner-managed, per-test object for a hand-managed, file-lifetime one, and every property the `page` fixture guaranteed goes with it. ## What is lost - **Independence.** One page means one context means one accumulating state. Test two starts wherever test one stopped. - **Order freedom.** The suite now has an implicit sequence. Reordering, filtering to one test, or running a subset changes outcomes. - **A clean re-run.** Re-running a failed test does not rebuild a page that was created in `beforeAll`, so the second attempt starts from the state the first one corrupted. - **Automatic cleanup.** The fixture closed itself; this page does not. It needs an `afterAll`, and a throw before that hook leaves it open. - **Honest failure messages.** The reported failure is often in the test that inherited bad state, not the test that created it, which sends people debugging the wrong file. ## The diagnosis signature You rarely meet this as "someone shared a page". You meet it as one of these: 1. A test passes when run alone and fails inside the file, or the exact reverse. 2. Moving a test up or down the file changes whether the suite is green. 3. A failure that only appears when a particular other test runs first. 4. A re-run of a failing test still fails while a fresh run of everything is green. Any of those means state is crossing a test boundary. The shared page is one of the two usual sources; the other is state parked outside the runner entirely, such as a module-level variable holding application data. | Property | The page fixture | A page from beforeAll | |---|---|---| | Lifetime | one test | the whole file | | Context | new per test | one for all tests | | Closed by | the runner | your afterAll hook | | Order sensitivity | none | high | ## Why teams do it anyway The motive is always speed, and it is a real motive. Signing in through the UI on every test in an internal admin console with several staff roles is genuinely slow. The mistake is the remedy: sharing a **live page** to avoid repeating a sign-in trades away isolation to save setup, when the setup itself is what should be made cheap. Playwright has a dedicated mechanism for reusing an authenticated session across tests without sharing a page, and that, not a file-level page, is where this pressure should be sent. ## Fixing it 1. Delete the shared variable and take `page` in each test's argument list again. 2. Move genuinely shared, read-only cost, such as producing a saved session or seeding reference data, into setup that produces **data** rather than a live page. 3. Re-run the file in a different order and with individual tests filtered, and confirm the results are stable. 4. If a case truly needs several pages at once, open them inside that one test from its own `context`, so the runner still owns the teardown. ## What to say Name the guarantee first, then the symptom: "The `page` fixture is test-scoped precisely so no test inherits another's state; a page created in `beforeAll` has file lifetime, so the suite becomes order-dependent, retries start dirty, and closing it is now my problem." That sequence, guarantee then consequence then fix, is what separates someone who has debugged this from someone quoting a rule.

  • The suite shares a page purely to avoid signing in every test. What do you offer instead?
    Keep the per-test `page` and make sign-in cheap rather than shared. Playwright supports reusing a previously saved authenticated session, so each test still gets its own context while skipping the slow UI login. Speed comes from cheaper setup, not from shared live state.
  • How would you prove to a sceptical team that the shared page is the cause?
    Run the failing test alone and watch it pass, then run it after its neighbour and watch it fail. Reordering the file changes the result too. That order sensitivity points at state crossing a test boundary, which the fixture would have prevented.
  • Is there any legitimate reason to call browser.newPage() in a test?
    Yes, when a single case genuinely needs more than one page at once. Do it inside the test rather than in beforeAll, ideally from the test's own context, so the runner still tears everything down at the end of that test.

saying these in an interview costs you the question

  • Calls a shared page a harmless speed optimisation
  • Adds sleeps instead of restoring isolation
  • Blames flakiness on the browser engine
  • Thinks a retry rebuilds a beforeAll page
  • Fixes order dependence by pinning test order
  • Assumes the runner closes a hand-made page