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.
| # | Journey | What it needs from the tool | In Playwright | Time |
|---|---|---|---|---|
| 1 | Search “blue jumper”, open SKU 30417, pay with the test card, get an order number | Typing into the card fields, which live in the payment provider’s iframe on another origin | frameLocator() reaches into a frame on another origin the way it reaches into the page; the order number is an assertion that waits for it | 90 s |
| 2 | Sign up, confirm the email, sign in, reload — still signed in. In Chromium and in WebKit | The same test in two browser engines | Two projects in the config (lines 2.2 and 2.3); one file, tagged @webkit, runs in both | 2 × 60 s |
| 3 | Reset a forgotten password from the emailed link | Reading a test inbox the browser never sees | The request fixture asks the inbox’s API, expect.poll repeats until the email is there, then page.goto() follows the link | 45 s |
| 4 | Apply SPRING10, change the quantity, pay | Comparing the screen with what the provider was asked to charge | request reads the charge from the sandbox’s API; the test compares it with the total on the page | 70 s |
| 5 | Pay with a card that needs 3-D Secure: out to the bank’s check and back | Leaving the shop’s origin and coming back into the same session | Nothing special: a context navigates across origins as a person does, and its cookies come back with it | 80 s |
| 6 | A card is declined; pay again with a second card | Exactly one order afterwards | request counts the customer’s orders through the API after the second payment | 75 s |
| 7 | Ask the order assistant “where is my order” about the order just placed | The assistant’s model, replayed from a recording — but the model is called by the server | Not page.route(), which sees only what the browser sends: the replay is set in the environment, and Playwright checks the status on the screen | 40 s |
| 8 | Start a return on day 30; the returns label arrives by email | Day 30 of the return window, in the browser and on the server | page.clock.setFixedTime() sets the browser’s date; the server’s test clock is set through its own API, which Playwright cannot reach | 60 s |
| 9 | The warehouse confirms the return arrived; the refund lands | Sending the warehouse’s webhook, then waiting for an email | request.post() sends the webhook from the test; expect.poll waits on the inbox with a timeout written for it (section 6) | 90 s |
| 10 | Change the delivery address after ordering, before dispatch | Reading what the warehouse received | Ordinary actions, then request reads the warehouse’s test endpoint | 50 s |
| 11 | Check out as a guest, then create an account from the confirmation page | Starting genuinely anonymous | Every test starts in a fresh browser context: no cookie and no storage left behind by journey 10 | 70 s |
| 12 | A support agent refunds a damaged item in the back office | A second identity, already signed in | The setup project signs the agent in once and saves the storage state (line 2.1); the test opens a context with it | 60 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.Where it runs
baseURLcomes from the environment built for this merge from the artefact that ships — never a shared test server.timezoneIdandlocaleare fixed, so a date and a price in euros format the same on every machine.- Analytics, maps and the marketing chat are blocked with
context.route()in a shared fixture: browser requests no journey’s outcome depends on.
2.Projects
setupsigns the support agent in and saves the state toplaywright/.auth/agent.json, which is in.gitignore.chromiumruns all twelve journeys on every merge, aftersetup.webkitruns journey 2 on every merge and all twelve at night;firefoxruns only at night.
3.How many at once
- One journey per file,
fullyParallel: true,workers: 4on CI/CDContinuous Integration / Continuous Delivery — automatic build, test and deliveryEvery commit is built and put through the tests automatically (CI), and a build that passes travels to the servers the same way (CD). The point is not automation for its own sake: it is that a breakage shows up in minutes instead of on release day.Full article →All terms — the size the test environment is built for. forbidOnly: trueon CI, so a forgottentest.onlyfails the run instead of quietly skipping eleven journeys.
- One journey per file,
4.When one fails
retries: 1on CI withretryStrategy: 'isolated', so a retry runs alone at the end, not beside the next journey.trace: 'on-first-retry', and the trace is uploaded as a build artefact.- A journey reported flakypasses and fails on the same codeA test that passes and fails on unchanged code. The real damage is not the failed run but what follows: once a suite has a few, people start re-running until green, and the suite stops being evidence of anything.Full article →All terms twice in a week goes to quarantine, the rule from Regression testing.
@playwright/testis 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 WebSocketa connection kept open both waysA connection that stays open in both directions: unlike HTTP, the server can send a message on its own without waiting to be asked. Chats and live feeds are built on it, and so is most of what looks like the page updating by itself.All terms. Every instruction goes down that connection, and every event comes back up it. For Chromium the language on the wire is the Chrome DevTools Protocolhow tools drive ChromeThe protocol Chrome’s developer tools use to inspect and control the browser, open to any program that connects to it. Test tools such as Playwright and Puppeteer drive Chromium through it, so they see the browser’s state directly rather than through the page.All terms. 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.
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 configure | What actually runs | Worth knowing |
|---|---|---|
chromium, headless — the default on CI | Chromium’s headless shell, a separate and lighter binary | Google 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 laptop | Chrome 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 machine | For a policy that says “test what customers run”, and for media codecs Chromium does not bundle |
firefox | Playwright’s patched Firefox, matching the current Firefox Stable | The branded Firefox cannot be driven |
webkit | Playwright’s patched WebKit, built from WebKit’s main branch | Often 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 UIUser Interface — what the user seesEverything the user sees and operates: screens, buttons, fields, states. UI is how the interaction looks; whether the interaction makes sense in the first place is a UX question, and the two get confused constantly.All terms 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 means | The moment in journey 1 it waits through |
|---|---|---|
| is exactly one element | The locator matches one element. None yet: it waits. Two: it fails at once with a strict-mode error | The page renders a Pay button for phones and one for desktops; the test fails instead of clicking whichever came first |
| is visible | A non-empty box and not visibility:hidden — an element at opacity:0 counts as visible | The basket summary is still collapsed |
| is stable | The same bounding box for at least two consecutive animation frames | The summary slides in and pushes the button down |
| receives events | It is the element a click at that point would actually hit | A spinner covers the button while the total is recalculated |
| is enabled | No disabled on it or on its fieldset, no aria-disabled above it | The button stays disabled until the card fields validate |
| is not inert | Not inside an inert subtree, and not behind an open modal <dialog> | The cookie-consent dialog is still open |
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,--shuffleruns tests in a random order with a seed to repeat it, which flushes out a test that quietly depends on another.
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.
| Need | In Playwright | Reaches | Does not reach |
|---|---|---|---|
| Arrange or check state | request, page.request | The shop’s APIs, the payment sandbox, the test inbox — journeys 3, 4, 6, 9, 10 | Anything without an API |
| Replace a third party | page.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 time | page.clock | Date, timers and animation frames inside the page | The server’s clock — day 30 must also be set on the back end (8) |
| A second user | browser.newContext() | Any number of isolated users in one test — journey 12 | Nothing: 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 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.
| Change | What it exposed | What protected a suite |
|---|---|---|
Playwright 1.49, November 2024: the chrome and msedge channels move to Chrome’s new headless mode | Suites 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 -k | The 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 Testing | Nothing 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 updated | Teams 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 iframea page inside a pageAn HTML element that embeds another document inside a page — a payment form, a video, a sign-in box. When it comes from another origin, the browser walls it off from the page around it, which is why a test tool may or may not be able to type into it.All terms 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
The tool, from its own documentation
the browsers, the checks, workers, the API client, the clock and traces in sections 2–6- DocumentationBrowsers (opens in a new tab)Playwright
- DocumentationAuto-waiting (opens in a new tab)Playwright
- DocumentationParallelism (opens in a new tab)Playwright
- DocumentationAuthentication (opens in a new tab)Playwright
- DocumentationAPI testing (opens in a new tab)Playwright
- DocumentationClock (opens in a new tab)Playwright
- DocumentationTrace viewer (opens in a new tab)Playwright
- DocumentationHeadless Chrome shell (opens in a new tab)Chrome for Developers
Research
the share of flaky UI tests that are waiting problems, in “Auto-waiting”The changes
the four rows of “Four changes underneath working suites”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.