QAAll levels

Playwright

Playwright is an open-source browser automation library and test runner from Microsoft that drives Chromium, Firefox and WebKit from outside the browser. The page opens with the twelve journeys from End-to-end testing written in Playwright — the mechanism each one leans on, and the config that runs them in about four minutes — then covers the one connection everything follows from, the browsers it actually runs, auto-waiting, contexts and workers, what a test can reach besides the page, traces and retries, four changes underneath working suites, and where it does not help.

Current version
1.64 · October 2026
Test runs
Outside the browser
Isolation unit
Browser context

The shop’s twelve journeys in Playwright on one screen — the mechanism each one leans on

Nobody opens a page about Playwright for its history. They open it with a suite to write or a suite to fix, and the useful question is concrete: what does each journey need from the tool, and which part of the tool supplies it? Below are the twelve journeys from the End-to-end testing page — the same online shop, the same times — as the shop writes them in Playwright. The fourth column names the mechanism each journey leans on. The sections after the table explain where each mechanism comes from, and they cite the journeys by number and the config below by line.

#JourneyWhat it needs from the toolIn PlaywrightTime
1Search “blue jumper”, open SKU 30417, pay with the test card, get an order numberTyping into the card fields, which live in the payment provider’s iframe on another originframeLocator() reaches into a frame on another origin the way it reaches into the page; the order number is an assertion that waits for it90 s
2Sign up, confirm the email, sign in, reload — still signed in. In Chromium and in WebKitThe same test in two browser enginesTwo projects in the config (lines 2.2 and 2.3); one file, tagged @webkit, runs in both2 × 60 s
3Reset a forgotten password from the emailed linkReading a test inbox the browser never seesThe request fixture asks the inbox’s API, expect.poll repeats until the email is there, then page.goto() follows the link45 s
4Apply SPRING10, change the quantity, payComparing the screen with what the provider was asked to chargerequest reads the charge from the sandbox’s API; the test compares it with the total on the page70 s
5Pay with a card that needs 3-D Secure: out to the bank’s check and backLeaving the shop’s origin and coming back into the same sessionNothing special: a context navigates across origins as a person does, and its cookies come back with it80 s
6A card is declined; pay again with a second cardExactly one order afterwardsrequest counts the customer’s orders through the API after the second payment75 s
7Ask the order assistant “where is my order” about the order just placedThe assistant’s model, replayed from a recording — but the model is called by the serverNot page.route(), which sees only what the browser sends: the replay is set in the environment, and Playwright checks the status on the screen40 s
8Start a return on day 30; the returns label arrives by emailDay 30 of the return window, in the browser and on the serverpage.clock.setFixedTime() sets the browser’s date; the server’s test clock is set through its own API, which Playwright cannot reach60 s
9The warehouse confirms the return arrived; the refund landsSending the warehouse’s webhook, then waiting for an emailrequest.post() sends the webhook from the test; expect.poll waits on the inbox with a timeout written for it (section 6)90 s
10Change the delivery address after ordering, before dispatchReading what the warehouse receivedOrdinary actions, then request reads the warehouse’s test endpoint50 s
11Check out as a guest, then create an account from the confirmation pageStarting genuinely anonymousEvery test starts in a fresh browser context: no cookie and no storage left behind by journey 1070 s
12A support agent refunds a damaged item in the back officeA second identity, already signed inThe setup project signs the agent in once and saves the storage state (line 2.1); the test opens a context with it60 s

The table says what each journey does. The config below says how all twelve run: where, in which browsers, how many at once, and what happens when one fails. Each line is a decision the shop made on purpose, and each has a section further down.

  1. 1.Where it runs

    1. baseURL comes from the environment built for this merge from the artefact that ships — never a shared test server.
    2. timezoneId and locale are fixed, so a date and a price in euros format the same on every machine.
    3. Analytics, maps and the marketing chat are blocked with context.route() in a shared fixture: browser requests no journey’s outcome depends on.
  2. 2.Projects

    1. setup signs the support agent in and saves the state to playwright/.auth/agent.json, which is in .gitignore.
    2. chromium runs all twelve journeys on every merge, after setup.
    3. webkit runs journey 2 on every merge and all twelve at night; firefox runs only at night.
  3. 3.How many at once

    1. One journey per file, fullyParallel: true, workers: 4 on — the size the test environment is built for.
    2. forbidOnly: true on CI, so a forgotten test.only fails the run instead of quietly skipping eleven journeys.
  4. 4.When one fails

    1. retries: 1 on CI with retryStrategy: 'isolated', so a retry runs alone at the end, not beside the next journey.
    2. trace: 'on-first-retry', and the trace is uploaded as a build artefact.
    3. A journey reported twice in a week goes to quarantine, the rule from Regression testing.
    4. @playwright/test is pinned to an exact version and upgraded in a change of its own, because an upgrade changes the browsers too.

One connection, out of process — and which browsers you are actually testing

Your test runs in a Node.js process of its own. Playwright launches a browser beside it and keeps one connection to that browser open for the whole run. For a browser it launched itself, the connection is a pipe between the two processes; to reach a browser on another machine, it is a . Every instruction goes down that connection, and every event comes back up it. For Chromium the language on the wire is the . For Firefox and WebKit, Playwright ships builds of its own, patched to speak its protocol.

That one arrangement explains the tool’s best-known properties. Because the driver sees the browser’s state directly rather than through the page, it can inspect an element before acting on it and wait until acting makes sense — section 3. Because everything crosses one connection, the whole run can be recorded and opened afterwards — section 6. And because the unit of isolation is a browser context rather than a browser, a clean session costs milliseconds instead of a browser launch, which is what makes real parallelism affordable — section 4.

Playwright runs your test in its own process and keeps one connection to a browser it launched for the whole run — a pipe between the two processes when the browser is local, a WebSocket when it is on another machine. Every instruction goes down that connection and every event comes back up it. For Chromium the language on the wire is the Chrome DevTools Protocol; for Firefox and WebKit, Playwright ships builds of its own with patches that speak its protocol, which is why it cannot drive the branded Firefox or the Safari on your machine. The tool's best-known properties follow from this one arrangement. Because the driver sees browser state directly, it can check that an element is ready before acting on it. Because everything crosses one connection, the whole run can be recorded as a trace and opened afterwards. And because the unit of isolation is a browser context — a private profile inside one running browser — a clean user costs milliseconds rather than a browser launch, which is what makes running journeys in parallel affordable.

The patches explain a fact that surprises people. Playwright does not drive the Firefox or the Safari installed on your machine: its documentation says it “doesn’t work with the branded version” of either, “since it relies on patches”. And “Chromium” means different binaries in different modes. The table is what actually runs.

You configureWhat actually runsWorth knowing
chromium, headless — the default on CIChromium’s headless shell, a separate and lighter binaryGoogle calls the new headless mode “the real Chrome browser” and more suitable “for high-accuracy end-to-end web app testing”; opt in with channel: 'chromium'
chromium, headed — your laptopChrome for Testing, since 1.57 (November 2025)The window you watch locally is not the binary CI ran headless
channel: 'chrome' or 'msedge'The branded stable browser installed on the machineFor a policy that says “test what customers run”, and for media codecs Chromium does not bundle
firefoxPlaywright’s patched Firefox, matching the current Firefox StableThe branded Firefox cannot be driven
webkitPlaywright’s patched WebKit, built from WebKit’s main branchOften ahead of Safari, and not Safari. On Linux, platform features such as media codecs differ from a Mac

Auto-waiting: six checks before every action, and the two things they cannot fix

The commonest reason a browser test is unreliable is that it acted or checked before the application was ready. In 2021 Romano and colleagues read 235 flaky tests from 62 web and Android projects and traced each to its cause. 105 of them, 45.1%, were waiting problems, and in the web projects alone the share was 52%. Older tools answered this with sleeps and hand-written waits, which is a guess dressed up as code. Playwright answers it with checks it runs before every action, against the element as it is at that moment, and repeats until they pass or the action times out.

Before a click, the element…What it meansThe moment in journey 1 it waits through
is exactly one elementThe locator matches one element. None yet: it waits. Two: it fails at once with a strict-mode errorThe page renders a Pay button for phones and one for desktops; the test fails instead of clicking whichever came first
is visibleA non-empty box and not visibility:hidden — an element at opacity:0 counts as visibleThe basket summary is still collapsed
is stableThe same bounding box for at least two consecutive animation framesThe summary slides in and pushes the button down
receives eventsIt is the element a click at that point would actually hitA spinner covers the button while the total is recalculated
is enabledNo disabled on it or on its fieldset, no aria-disabled above itThe button stays disabled until the card fields validate
is not inertNot inside an inert subtree, and not behind an open modal <dialog>The cookie-consent dialog is still open
Before a click, Playwright checks the element six times over: that the locator matches exactly one element, that it is visible, that it is stable — the same bounding box for two animation frames in a row — that it is the element a click at that point would actually hit, that it is enabled, and that it is not inert, inside an inert subtree or behind an open modal dialog. Each box names the moment of the shop's checkout it waits through: the collapsed basket summary, the summary sliding in and moving the button, a spinner over the button while the total is recalculated, the button disabled until the card fields validate, the cookie dialog still open. If any check fails, Playwright waits, finds the element again from the locator and checks again, until every check passes and it clicks once, or the action times out with an error that names the check that failed. One case does not wait at all: if the locator matches two elements, such as a Pay button for phones and one for desktops, the click fails at once with a strict-mode error rather than clicking whichever came first.

The second half of the model is the locator. A locator is not an element. It is a description of how to find one, resolved again every time it is used, so on a page that re-renders underneath the test it finds the current element rather than holding a node that no longer exists. Assertions on a locator work the same way: they retry until the condition holds, for five seconds by default. For a condition that is not on the page at all — an email in a test inbox, a record in the warehouse — expect.poll repeats a function until its result passes, which is how journeys 3 and 9 wait without a single sleep.

Contexts, workers and the four-minute run

Every test gets a fresh browser context by default: no shared cookie, no leftover storage, no order dependency between journeys. This is the quiet reason Playwright suites stay stable as they grow. The commonest source of “it passes alone and fails in the suite” is removed by the default rather than by discipline.

Parallelism comes from workers. A worker is an operating-system process with its own browser; workers cannot talk to each other, and one is shut down after any test failure, so the next test never inherits a broken browser. Test files run in parallel across workers, while the tests inside one file run in order in one worker — unless fullyParallel is on. The shop keeps one journey per file and turns it on anyway (config line 3.1), so the unit the scheduler hands out is one journey.

  • Sign in once, reuse everywhere

    A setup project signs in and saves the storage state; tests that need that identity start with it. The documentation recommends it for tests that do not change server-side state, and one account per worker for tests that do.

  • Fixtures instead of setup code

    A fixture is a named thing a test asks for — a signed-in page, a seeded order, an API client. It is built on demand and torn down after, so a journey declares what it needs instead of arranging it.

  • Locks for what cannot be shared

    Since 1.63 a test can hold a named lock: tests that share it never run at the same time, across files, workers and projects, while everything else stays parallel. It is for the one external account that cannot take two writers.

  • Shards, and shuffling to find a dependency

    The same suite splits across CI machines with --shard=1/3, and the reports merge afterwards. Since 1.64, --shuffle runs tests in a random order with a seed to repeat it, which flushes out a test that quietly depends on another.

The shop's suite on four Playwright workers. A setup project signs the support agent in first, for about 15 seconds, because both browser projects depend on it. Then the thirteen tests — the twelve journeys in Chromium, with the times from the End-to-end testing page, and journey 2 again in WebKit — are handed in file order to whichever worker is free first. Worker 1 runs journeys 1, 8 and 12 and finishes at 225 seconds; worker 2 runs 2, 6 and 11 and finishes at 220; worker 3 runs 3, 5, 10 and journey 2 in WebKit and finishes last, at 250; worker 4 runs 4, 7 and 9 and finishes at 215. That is 865 seconds of work in a little over four minutes. The run is as long as the busiest worker, not the sum of the tests, so more workers would shorten it until every journey had its own — the floor then is the setup plus the longest journey, 105 seconds. In practice the limit is the test environment: the shop's is built for four customers at once.

What a test can reach besides the page — and where the reach ends

Three capabilities beyond clicking change what belongs in a journey at all. The first is the API client. The request fixture sends requests from the test, and page.request shares the browser context’s cookies in both directions, so a journey can arrange its state over the API in fifty milliseconds and then check one thing in the browser. The second is network routing: requests the page makes can be observed, delayed, answered from a recorded HAR file or blocked. The third is the clock, which replaces the page’s Date, timers and animation frames with a clock the test controls.

NeedIn PlaywrightReachesDoes not reach
Arrange or check staterequest, page.requestThe shop’s APIs, the payment sandbox, the test inbox — journeys 3, 4, 6, 9, 10Anything without an API
Replace a third partypage.route(), context.route(), routeFromHAR()Requests the browser makes — analytics, maps (config line 1.3)Calls one server makes to another: the assistant’s model (7), the carrier’s labels (8)
Control timepage.clockDate, timers and animation frames inside the pageThe server’s clock — day 30 must also be set on the back end (8)
A second userbrowser.newContext()Any number of isolated users in one test — journey 12Nothing: this is where the tool is strongest

A failure you can open: traces, retries and the flaky label

The expensive part of an end-to-end failure is rarely the failure. It is the hour spent trying to reproduce it on a laptop where it does not happen. The trace is the answer, and it is the setting worth getting right before any other. A trace records every action with a snapshot of the page before and after it, the network log, the console and the source line. The shop records one on the first retry (config line 4.2) and uploads it with the build. It opens locally or at trace.playwright.dev, which, the documentation says, loads the trace entirely in the browser and transmits nothing.

The trace of journey 9's retry, the run the report labelled flaky. The test posts the warehouse's webhook in the first third of a second, and the page shows the refund as issued at 1.4 seconds — so the shop did its job. The test then polls the test inbox every half second with expect.poll, whose default limit is 5 seconds. Eight requests come back empty; the email arrives at 4.6 seconds and the retry passes with 0.4 seconds to spare. The first attempt had hit the limit at 5.0 seconds and failed. Nothing had to be reproduced on a laptop: one file shows the refund on screen, the inbox empty until 4.6 and the limit at 5.0. The diagnosis is that the timeout is wrong, not the product, and the fix is a limit written for the condition — 15 seconds, because the email pipeline's 99th percentile is about nine — rather than a sleep in front of the poll.

The trace in the diagram is journey 9 on a Tuesday. Its first attempt failed; the retry passed, so the report says flaky — and the retry is the run the shop records a trace for. It shows the refund on the page at 1.4 seconds, so the shop did its job. It shows the inbox poll coming back empty eight times and the email arriving at 4.6 seconds, against the poll’s default limit of 5.0. The first attempt simply ran out of those last 0.4 seconds. Nobody had to reproduce anything, and the diagnosis is the timeout, not the product. The fix is a limit written for the condition — 15 seconds, because the email pipeline’s 99th percentile is about nine — and never a sleep in front of the poll.

Retries are where a suite quietly lies. Playwright labels a test that failed and then passed on retry as “flaky” rather than “passed”, and by default the run still exits green; --fail-on-flaky-tests makes it red. The shop keeps one retry, runs it alone at the end with retryStrategy: 'isolated' so a retry does not compete with the next journey for the environment, and counts the flaky labels as a signal of their own (config lines 4.1 and 4.3). Since 1.57 the HTML report also has a Speedboard tab that sorts tests by slowness — the first place to look when four minutes becomes seven.

  • Use the interactive mode, --ui, while writing a journey: the run pauses, the locator under the cursor is highlighted, and another one can be tried against the live page.
  • Treat recorded code as a draft. Recording a flow produces working locators fast, and they are almost always more brittle than the ones you would choose deliberately.
  • Since 1.56 Playwright ships three agent definitions for coding assistants: a planner that explores the app and writes a test plan, a generator that turns the plan into tests, and a healer that runs the suite and repairs failing tests.

Four changes underneath working suites

None of these is a scandal. Each is a dated change in Playwright or in the browsers under it, published by the people who made it, and each one sorted the suites that had pinned and upgraded on purpose from the ones that had not. They are here because a browser test depends on more than its own code, and these four show where.

ChangeWhat it exposedWhat protected a suite
Playwright 1.49, November 2024: the chrome and msedge channels move to Chrome’s new headless modeSuites on those channels were now running a different headless browser. The release notes: if the suite fails, “you will probably need to update your snapshots”, and code around PDF viewers and extensions needs adapting.An upgrade as a change of its own, with screenshot baselines treated as belonging to a browser build rather than to the product.
CVE-2025-59288, 14 October 2025: the macOS scripts that install branded Chrome and Edge used curl -kThe scripts behind installing chrome and msedge on a Mac skipped certificate checks, and, the advisory says, “the downloaded files are installed as privileged applications”. Fixed in 1.55.1.Treating the browser download as part of the supply chain: a pinned version upgraded on purpose, and CI images built once and reused instead of installing browsers on every run.
Playwright 1.57, November 2025: Chromium builds replaced by Chrome for TestingNothing functional, by design: headed runs moved to Chrome for Testing’s chrome and headless runs to its headless shell, and the notes said “the biggest change is the new icon and title in your toolbar”.Nothing was needed — which is the point of the row. A suite that pins and upgrades on purpose absorbs a change of browser build as a non-event, and knowing which binary each mode runs (section 2) keeps “passes locally, fails on CI” a short conversation.
Playwright 1.63, September 2026: the experimental component-testing packages stop being updatedTeams with component suites on @playwright/experimental-ct-react or -vue had to move to the stories model introduced in 1.62.Reading “experimental” in a package name as the promise it is, which is none — and keeping a small exit, such as every component test mounting through one helper, so a move takes a day rather than a quarter.

Where it does not help, and how it compares

Playwright is the reasonable default for a new browser suite in 2026, and a default deserves its caveats stated plainly rather than discovered in month three.

  • WebKit is not Safari. It is the same engine, patched and built from WebKit’s main branch, without Apple’s browser around it — it catches engine differences, and not everything specific to the Safari customers run.
  • Mobile emulation is a screen size, a user agent and touch — not a device. It finds layout problems and will not find a bug in a real mobile browser.
  • Its reach ends at the browser’s side of the network. Calls between servers and the server’s own clock belong to the environment, not to the test.
  • It does not test native desktop or mobile applications, and it is not a load testing tool, however tempting a hundred parallel contexts look.
  • It makes flaky tests less likely; it does not make a badly chosen suite good. Four hundred end-to-end cases are wrong here for exactly the reasons they are wrong anywhere else.

Choose Playwright when

  • You are starting a suite now and no constraint pushes you elsewhere.
  • A journey needs several users, several origins, a payment or several tabs.
  • Suite runtime matters: parallelism per context is cheap, and sharding is built in.
  • You want a CI failure diagnosable from one file, without reproducing it.

Look elsewhere when

  • You must run against the branded browsers customers have — real Safari above all — on a grid of real machines; that is Selenium’s ground.
  • Your tests are written in a language Playwright does not ship, or the team already lives in a Selenium stack.
  • A large working Cypress suite exists — the migration usually costs more than it returns.
  • The thing to test is a native application, a desktop client or system-level behaviour.

Anti-patterns worth naming

  • Explicit waits “just in case” on top of auto-waiting, which slow the suite and hide the real timing question.
  • Locators by CSS class or DOM position because the recorder produced them, which turns every redesign into a day of test repair.
  • One context reused across tests to save time, which brings back the order dependency the tool was designed to remove.
  • Retries raised until the pipeline is green, which converts real intermittent defects into passing runs.
  • An unpinned @playwright/test, so the browsers change on a day nobody chose.
  • Unit-level logic moved into browser tests because the tooling is pleasant — the most expensive way there is to be thorough.

When to use it

Use it when

  • A new browser suite where reliability and runtime both matter and no language constraint applies.
  • Journeys with more than one user, more than one origin, a payment iframe or more than one tab.
  • Suites that must run on every merge, with workers on one machine and shards across several.
  • Wherever Chromium, Firefox and WebKit should run from the same test file, pinned to one version.

Avoid it when

  • As proof that a page works in the shipped Safari or a real mobile browser — WebKit and emulation do not cover that.
  • For behaviour that lives between servers, or on the server’s clock — the test cannot reach it.
  • For native desktop or mobile applications, or as a load testing tool.
  • As a reason to grow the end-to-end suite, since a pleasant tool does not change what belongs at that level.

Sources and method

What this page stands on

  • 11 sources
  • Checked October 19, 2026

Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.

Found this useful?

Share it with someone who is working on the same problem.