skip to content

In a Playwright port, what replaces the setUp and tearDown that created and quit a browser driver?

level: middleimportance: must knowfreq 55%

answer

  1. The runner owns the browser lifecycle now
  2. One fresh profile per test, not per class
  3. No cookie clearing between cases
  4. Hooks keep application setup only
  5. Ask for browser to build a second session

basics

~20 s

The runner does it. Playwright Test injects a page fixture backed by a fresh browser context per test and closes it afterwards, so the browser creation and quit code is deleted; beforeEach keeps only application setup such as signing in.

solid answer

~50 s

The lifecycle stops being test code. Declaring `async ({ page }) => { ... }` asks Playwright Test for the built-in `page` fixture; the runner reuses one browser per worker process, opens a **fresh `BrowserContext`** for each test, hands you a page in it, and closes the context when the test ends — pass or fail. A context is an isolated profile with its own cookies and storage, and it is cheap enough to create per test, so the old "one browser for the whole class, quit at the end" pattern is not worth porting. `test.beforeEach` survives only for application-level setup such as logging the broker in or seeding a quote; `test.afterEach` for data your test created. When one test genuinely needs a second isolated session, ask for the `browser` fixture and call `browser.newContext()` yourself.

code

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

// No browser is created or quit here: the page fixture owns that lifecycle.
test.beforeEach(async ({ page }) => {
  await page.goto('https://quotes.example.com/login');
  await page.getByLabel('Broker ID').fill('BR-1042');
  await page.getByRole('button', { name: 'Sign in' }).click();
});

test('broker sees the saved quotes', async ({ page }) => {
  await expect(page.getByRole('heading', { name: 'Saved quotes' })).toBeVisible();
});

test('a second session starts signed out', async ({ browser }) => {
  const underwriter = await browser.newContext();
  const review = await underwriter.newPage();
  await review.goto('https://quotes.example.com/review');
  await expect(review.getByRole('heading', { name: 'Sign in' })).toBeVisible();
  await underwriter.close();
});

go deeper

for a junior

Learn to delete rather than translate here. Take the page parameter, use it, and write no code that opens or closes a browser; the runner handles both ends for every test.

for a middle

Explain the split: one browser per worker process, one fresh context per test, and the context is what carries cookies and storage. That is why isolation is automatic and cheap.

for a senior

Watch what the port drops. Hooks should keep only application setup and data cleanup, and a suite that still shares one session across cases will hide ordering defects the old suite was living with.

for a principal

Set the boundary for the migration. Lifecycle code is the runner's job, and any exception — a second session, unusual context options — should be justified per test rather than reinstated as a shared base class.

In a legacy suite the browser lifecycle is written by hand: a setup method constructs a driver, tests share it through a field, and a teardown method quits it. Porting that literally is the single most common mistake in a migration, because the new runner already owns the lifecycle. ## What the old lifecycle was really doing Three separate jobs are usually tangled into `setUp`/`tearDown`: 1. **Creating a browser process**, which is expensive, so the suite tends to share one across many tests. 2. **Establishing isolation** — clearing cookies, wiping local storage, or opening a private window — because a shared browser leaks state between cases. 3. **Application setup**, such as signing the broker in and landing on the quotes dashboard. Playwright takes over the first two. Only the third stays yours. ## The page fixture and the context behind it Writing `test('...', async ({ page }) => { ... })` is a request for the built-in `page` fixture. For each test the runner: - reuses a **browser** already launched for the worker process, rather than starting one per test; - creates a **new `BrowserContext`** — an isolated profile with its own cookies, local storage, permissions and cache; - opens one `Page` in it and passes it to the test body; - closes the context after the test, in either outcome, which closes its pages with it. A context is not a browser: creating one is closer to opening a private window than to starting a process, which is why per-test isolation is affordable and why nothing needs to be cleared between cases. | Legacy construct | Ported form | |---|---| | field holding the driver | the `page` fixture parameter | | `setUp` creating the driver | nothing — the runner does it | | `tearDown` quitting the driver | nothing — the context closes automatically | | clearing cookies between tests | nothing — each test gets a fresh context | | a second browser for a second user | `browser.newContext()` inside the test | ## What is left for beforeEach `test.beforeEach` is still the right home for the application-level part of the old setup: navigating to the entry point, signing in, or creating the record under test through the API. Two things to watch while porting: - A hook that takes `{ page }` gets **the same page instance the test will get**, so setup performed there is visible to the test — this is what makes a login hook work. - `test.afterEach` should clean up *application* state you created, not browser state. Deleting a quote your test generated is real cleanup; closing the page is not, and closing it yourself only breaks the runner's own teardown and artefact capture. ## Porting the insurance-quote base class A typical port of a shared base class goes like this: 1. Delete the driver field, the `setUp` that built it and the `tearDown` that quit it. 2. Move the sign-in steps into a `test.beforeEach` that takes `{ page }`. 3. Delete every cookie- or storage-clearing call, since a new context starts empty. 4. Replace any "open a second browser for the underwriter" code with `browser.newContext()` plus `context.newPage()`, closing that context at the end of the test. 5. Leave a repeated sign-in as-is at first; collapsing it into a saved session or a custom fixture is a later, separate improvement. ## When you still create a context yourself Ask for the `browser` fixture and build the context by hand when a single test needs **two independent sessions** — a broker and an underwriter looking at the same quote — or when one test needs different context options from the rest of the file. That is the only remaining reason to write lifecycle code, and it is per-test rather than per-suite. Everything else the ported suite used to do around the driver becomes, quite literally, nothing.

  • If the browser is shared across tests in a worker, how are two tests still isolated from each other?
    Isolation lives in the context, not the process. Each test gets its own `BrowserContext` with empty cookies, storage and permissions, and it is discarded afterwards. Sharing the browser only saves the launch cost; nothing written by one test's context is visible to the next.
  • Should the ported test close the page it was given at the end?
    No. The runner closes the context, and its pages with it, after the test finishes either way. Closing the injected page yourself is redundant and can cut short trace, video and screenshot capture, leaving a failure harder to diagnose than before the port.

saying these in an interview costs you the question

  • Porting the driver field and quitting it in afterEach
  • Clearing cookies between tests that already get a fresh context
  • Assuming each test launches its own browser process
  • Closing the injected page at the end of the test
  • Sharing one page across tests to save start-up time