skip to content

Moving an Existing Suite

What survives a port: finders and assertions map over, most explicit waiting disappears, and a driver plus its lifecycle becomes a context plus a fixture. Asked as a planning question.

on this pageshow

explore

questions

5

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
open as a page

Porting an old browser suite to Playwright, what replaces its explicit waits and sleeps?

level: middleimportance: must knowfreq 62%

basics

~20 s

Most of them disappear. A Playwright locator is re-resolved every time it is used, and actions wait for the element to be ready, so waits for a state become retrying assertions like expect(locator).toBeVisible(). Fixed sleeps are deleted, not translated.

open as a page

When moving a legacy suite to Playwright, how do you decide what a faithful port means?

level: principalimportance: should knowfreq 41%

basics

~20 s

Preserve intent, not implementation. Port each case so it asserts the same thing about the product, but never translate sleeps or hand-written browser lifecycle code, and restructure only where the old test fights Playwright's model rather than everywhere at once.

open as a page

What does the --target flag on Playwright's codegen command change about the script it emits?

level: juniorimportance: nice to knowfreq 33%

basics

~20 s

It picks the output language and test harness. Playwright codegen prints the same recorded steps as Playwright Test JavaScript by default, or as python-pytest, java-junit or csharp-nunit, so a porting team gets a starter script in its own language.

open as a page

What does setting SELENIUM_REMOTE_URL do to a Playwright run during a migration?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

It routes Chromium through a Selenium Grid hub instead of launching it locally. Playwright asks the grid for a browser session and drives it over the Chrome DevTools Protocol. The bridge is experimental and covers Chromium only.

open as a page