skip to content

Driving Model Contrasts

It drives a browser build it ships itself, over one long-lived connection, from outside the page. Interviewers ask because nearly every feature and every limit follows from that single choice.

on this pageshow

explore

questions

5

How does Playwright's test process control the browser it automates?

level: juniorimportance: must knowfreq 82%

answer

  1. Two processes, not one
  2. Test code never runs in the page
  3. One connection for the whole session
  4. Handles, not DOM nodes
  5. Browser build ships with the library

basics

~20 s

Playwright test code runs in its own Node process, outside the browser. The library launches a browser build it ships itself and drives it over one long-lived connection that carries every command and every event for the whole session.

solid answer

~50 s

Playwright is a client and a server joined by one connection. Your test file runs in a Node process; the browser is a separate process running a build Playwright downloaded into its own cache. The library opens a single duplex connection to it when the browser starts (a pipe locally, a WebSocket when you attach to a remote browser with `browserType.connect()`), and that one connection carries commands out and events and results back until the browser closes. Nothing in your test process is a DOM node: `browser`, `context` and `page` are handles to things living at the far end, which is why every read is awaited. Because the connection is already open, one more command costs a message rather than a fresh handshake, and that cheapness is what the rest of the feature set is built on.

code

typescript · 9 lines
typescript
import { chromium } from '@playwright/test';

const browser = await chromium.launch();   // starts a bundled build, opens one connection
const page = await browser.newPage();      // a handle to a tab in that browser

await page.goto('https://quotes.example.com/motor');
console.log(await page.title());           // fetched across the connection, not read locally

await browser.close();                     // connection closes; every handle above is now dead

go deeper

for a junior

Remember the shape: your test runs in Node, the browser runs beside it, and one connection joins them. That is why nearly every call is awaited - the answer has to come back from the other process.

for a middle

Be able to explain the connection itself: opened once when the browser starts, duplex, carrying commands out and events and results back, so an extra call costs a message rather than a handshake.

for a senior

Show that you reason about failures through the model. A dead connection kills every handle at once, and a timeout tells you the browser side never reached a state, not that the wire was slow.

for a principal

Own the consequences of the split: which process holds the state you need for diagnosis, what a shared long-lived browser does to blast radius, and where connecting to a browser you did not launch moves the risk.

Playwright's whole feature set follows from one architectural choice: **your test code runs outside the browser, and talks to a browser Playwright launched itself over a single persistent connection.** ## Two processes, one connection A run always has at least two processes. - The **test process** executes your test file, your fixtures and your assertions. It is ordinary Node code. - The **browser process** is a browser build that Playwright downloaded and keeps in its own version-scoped cache, started by the library rather than by you. Between them sits **one long-lived duplex connection**, opened when the browser starts and closed when it exits. Locally it is a pipe to the browser process; when you attach to a browser running elsewhere with `browserType.connect()` it is a WebSocket. Either way, it is opened once. Nothing reopens per call. ## What travels over it - **Commands** go out: navigate here, click the element matching this description, report this page's title. - **Events** come back: a page opened, a request was issued, a dialog appeared, the browser crashed. Handlers you register with `page.on(...)` are fed by that stream rather than by polling. - **Results** come back too, each correlated to the command that asked for it. Because the socket is already up, an extra command costs a message, not a connection. That single property is what makes per-action readiness gating, per-test isolation, `page.route()` interception and trace capture affordable rather than exotic. ## What your objects actually are Nothing in the test process is a live DOM node. | Your test holds | What it really is | |---|---| | `browser` | a handle to the browser process at the far end of the connection | | `context` | a handle to one isolated profile inside that browser | | `page` | a handle to one tab inside that context | | `locator` | a stored description, re-resolved in the browser on every use | Every value you read - a title, a text string, a bounding box - is fetched across the connection at the moment you ask for it. That is why the API is asynchronous end to end: the data does not exist locally until a round trip brings it back. ## One action, end to end 1. Your test calls `page.getByRole('button', { name: 'Recalculate premium' }).click()`. 2. The test process sends **one** command naming the element description and the action. 3. The browser side resolves that description, waits for the element to be ready, and performs the click. 4. A result comes back on the same connection - or a timeout error that names what the browser was still waiting for. ## Why "outside the page" is the load-bearing part Your test code is never a script inside the document under test. It therefore survives navigations and reloads, is untouched by the application's own JavaScript errors, and is not confined to one origin or one tab. The application cannot call into it, and a bundler or a strict Content-Security-Policy has nothing of yours to trip over. The flip side is that anything you want to know about the page has to be asked for explicitly, and anything you hand to `page.evaluate()` really does cross into the page, where it runs under the page's rules instead of yours. ## Where people get this wrong - Using a `page` handle after `browser.close()`. The far end is gone, and Playwright reports `Target page, context or browser has been closed`. - Expecting `console.log` inside a `page.evaluate()` callback to appear in the terminal. That callback runs in the browser; subscribe with `page.on('console')` to see it. - Assuming Playwright automates the Chrome already installed on the machine. By default it drives the build it downloaded; using an installed branded build is an explicit choice through the `channel` launch option. - Thinking a slow test is slow because of chatter on the connection. Local messages are cheap; the time is nearly always in the application, in navigation, or in a readiness gate that never passed.

  • What happens to your pages and contexts when that connection goes away?
    They die with it. Handles are references into the browser process, so once the browser exits or the connection drops, any further call fails with `Target page, context or browser has been closed`. Pending actions reject rather than hang, and anything you wanted from the session - state, screenshots, a trace - has to have been collected before the close.
  • Does connecting to an already-running browser change the model?
    No. `browserType.connect()` and `connectOverCDP()` swap a local pipe for a WebSocket to a browser somewhere else; the client and server split, the handle semantics and the command and event flow are identical. What changes is ownership: you did not launch that browser, so it may outlive your test and may not be the build Playwright pins.

It is a phone line kept open for the whole call rather than a letter posted for each instruction: the line is already up, so one more request is a sentence instead of a new envelope.

saying these in an interview costs you the question

  • Thinks the test script runs inside the page under test
  • Believes each command opens a new connection or session
  • Says locators hold DOM nodes in the test process
  • Assumes Playwright drives whatever browser is installed
  • Expects page handles to work after the browser closes
open as a page

Why can Playwright retry an action's readiness checks without paying a round trip per attempt?

level: middleimportance: must knowfreq 66%

basics

~20 s

Because the retrying happens on the browser side. One command crosses the already-open connection, and Playwright's browser-side code re-resolves the element and re-checks readiness in a loop until the action succeeds or that command's timeout expires.

open as a page

Why can one Playwright test drive two origins, a popup and its network traffic at once?

level: middleimportance: should knowfreq 55%

basics

~20 s

Because Playwright drives the browser from outside the page. The connection is to the browser, not to one document, so another origin, an extra tab and the browser's network layer are all in reach of the same test.

open as a page

How does Playwright's driving model let a legacy quote suite get isolation without a browser launch per test?

level: seniorimportance: should knowfreq 44%

basics

~20 s

One connection serves one browser process that can hold many isolated profiles at once. Isolation costs a fresh context, created in milliseconds, instead of a process launch, so the expensive part is paid once per worker rather than once per test.

open as a page

Playwright pins the browser builds it downloads; what does that buy a team and what does it cost?

level: principalimportance: should knowfreq 34%

basics

~20 s

It buys reproducibility: every machine drives the same build, so one variable leaves flake triage. It costs coverage and control - you exercise the build Playwright ships rather than the one users run, and a library upgrade moves the browsers with it.

open as a page