Home / Blog / Playwright advantages and disadvantages

Playwright: advantages and disadvantages

Quick answer

Playwright waits for elements to be actionable before it acts, retries assertions until they pass, and isolates each test in a browser context that costs far less than a browser launch. It drives browsers only: no native mobile apps, no load testing, and its WebKit build is not the Safari on anyone's iPhone.

Every limitation on this page is in Microsoft's own documentation. They sit on different pages: a browsers guide, an API class reference, an options table inside that reference, a paragraph about supported languages. A team can read for an afternoon, adopt the framework, and still meet one of them in week three.

What is Playwright good at, and why?

Each of the mechanisms below deletes something a team currently writes by hand or debugs by hand. If you want the definition before the argument, it is in what Playwright is.

The wait belongs to the action

Playwright runs actionability checks before it acts and only performs the action once the relevant ones pass. A locator.click() will not fire until the element is visible, has stopped moving, is not covered by something else and is enabled. A locator.fill() drops two of those and adds editable, because typing into a field cares about different things from clicking one.

So the wait is a property of the action rather than a line above it. There is no sleep to tune, no helper class that everybody imports, and no explicit-wait timeout that somebody set to ten seconds in 2021 and nobody has dared lower since.

import { test, expect } from '@playwright/test';

test('publishing shows the live badge', async ({ page }) => {
  await page.goto('https://example.test/posts/42');
  await page.getByRole('button', { name: 'Publish' }).click();

  await expect(page.getByRole('status')).toBeVisible();
});
Playwright 1.62 · TypeScript. Samples on this page are written against 1.62.

The badge does not exist in the DOM at the moment that last line runs, and the test has nothing in it that says so.

The assertion owns the polling loop

Web-first assertions re-fetch the element and re-check the condition until it holds or the assertion timeout expires, which the documentation puts at five seconds by default and which the timeouts page repeats as 5,000 ms. toBeVisible(), toHaveText(), toContainText(), toHaveAttribute() and toBeChecked() all behave that way. The generic matchers do not, so expect(count).toBe(4) runs once and means what it says.

The practical effect is that a slow render stops being a test failure and becomes a slow test. The failures that remain are usually about the application.

Isolation costs a context, not a browser

Each test gets its own browser context, which the documentation describes as an incognito-like profile with its own cookies, local storage and session storage, and calls "fast and cheap to create and are completely isolated, even when running in a single browser." A clean session per test and a suite that runs in parallel therefore stop being in tension, because you are not paying for a browser launch to get the clean session.

The test answers the network itself

page.route() intercepts a URL pattern before the request leaves, route.fulfill() answers it with a body you wrote, and route.abort() kills it outright. context.route() does the same across every page in a context, and page.waitForResponse() holds the test until a real response arrives.

import { test, expect } from '@playwright/test';

test('shows the empty state when the API returns nothing', async ({ page }) => {
  await page.route('**/api/invoices', route => route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ invoices: [] }),
  }));

  await page.goto('https://example.test/invoices');
  await expect(page.getByText('No invoices yet')).toBeVisible();
});
Playwright 1.62 · TypeScript

The empty state, the 500 and the response that takes four seconds are all testable from the spec file. No proxy, no seeded fixture server, and no waiting on a staging environment that somebody else is also using.

A CI failure arrives as something you can step through

Put trace: 'on-first-retry' in the config and a test that fails, retries and fails again writes a trace.zip. npx playwright show-trace opens it, and so does dropping the file onto trace.playwright.dev. You get the actions, the DOM as it was before and after each one, and the source line that ran.

That changes where an investigation ends. "It only fails on CI and I cannot reproduce it" is normally the last sentence anyone writes on the ticket. With the artifact in hand it is the first one.

The first test can be recorded

npx playwright codegen https://example.test opens a browser, follows you through a flow and writes the spec, choosing locators as it goes. The output is a first draft and it wants editing before it is merged. For a team whose tests/ directory is empty, it beats starting at a blank file, and it settles arguments about locator style by producing something concrete to argue with.

Three engines from one API, and what the WebKit one covers

One configuration file runs the same spec against Chromium, Firefox and WebKit, and the channel option reaches branded Google Chrome and Microsoft Edge on the machine, which the documentation notes Playwright does not install for you. Running all three on every commit is worth having. The WebKit half comes with a qualification: Playwright's WebKit is a build it ships and patches, and the browsers guide says that is why branded Safari is out of its reach. A project named webkit gives you a WebKit build on your own machine. A coverage matrix that writes "Safari" in that row is telling its owner something untrue, and the person who signed the matrix is rarely the person who wrote the config.

If your question at this point is "compared with what", the closest comparison is against Cypress.

What can Playwright not do?

Each of the limits below is a property of the framework, and none is a gap somebody is planning to close.

It does not test native mobile applications

Playwright drives browsers. An application somebody installs from a store has screens that no browser renders, and no amount of configuration reaches them.

Playwright's answer for mobile is device emulation, which is one line of configuration and looks like coverage:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'desktop', use: { ...devices['Desktop Chrome'] } },
    { name: 'phone',   use: { ...devices['iPhone 13'] } },
  ],
});
Playwright 1.62 · TypeScript

That second project runs one of Playwright's own browser builds, on the machine running the tests, configured with a phone's user agent, screen size, viewport, device scale factor and touch flag. Locale, timezone and geolocation are on the same switch. Responsive layout, tap targets and touch behaviour are all genuinely tested that way. The store build, the native navigation, the permissions dialog and the device itself are not.

Android is the part that gets quoted back, so be exact about it. There is a real Android API, and its reference opens by saying "Playwright has experimental support for Android automation. This includes Chrome for Android and Android WebView." That is a browser and a web view, driven over ADB on a physical device. It is still not your native UI.

A React Native, Swift or Kotlin application needs Appium or a device cloud. That is a second tool sitting alongside Playwright, not a plugin inside it.

Its WebKit is not the Safari your users have, and there is no Internet Explorer

Playwright's WebKit build is derived from the latest WebKit main branch sources, often ahead of what has reached Apple Safari, and that lead is worth something: engine-level breakage turns up in your pipeline before it turns up in a customer's browser. The browsers guide puts the other half in one sentence — "Playwright doesn't work with the branded version of Safari since it relies on patches."

So testing on WebKit catches most WebKit-shaped bugs and is worth doing on every commit. It is not a statement about Safari on an iPhone, and a contract that names real iOS devices needs devices.

The distinction is about Safari specifically. Branded Chrome and Edge are reachable through channel: 'chrome' and channel: 'msedge', with beta, dev and canary variants listed alongside them, so "Playwright cannot drive branded browsers" is the wrong generalisation to carry away.

Internet Explorer is not on the supported list, which is Chromium, WebKit and Firefox plus those Chrome and Edge channels.

It is not a load-testing tool

This one is an argument from architecture rather than a quotation, because documentation describes what a tool does. The unit of work in Playwright is a browser context inside a real browser process, and concurrency comes from worker processes on the machines running the tests. Every simulated user therefore costs you a browser.

There is no virtual-user model, no ramp profile, no think-time and no aggregated percentile report, which are the parts a load tool is made of. Five thousand concurrent users is a different cost model rather than a larger number in a config file. Firm86 does not sell a load-testing engagement, in Playwright or otherwise.

What does Playwright cost you that a feature table does not show?

A comparison chart shows none of this, and every item here turns up somewhere between week two and the first upgrade.

The test runner is JavaScript and TypeScript only

"Playwright" names two products under one word. The library, which is the browser automation API, has four official bindings, each with its own Microsoft repository. The test runner has one.

The supported-languages page says what each binding runs tests with, and the split is plain once you read it side by side. Node.js comes with its own test runner. For Python, the Pytest plugin is the recommended way to run end-to-end tests. Java tells you to choose any testing framework such as JUnit or TestNG. .NET ships MSTest, NUnit, xUnit and xUnit v3 base classes. The same page frames it as core browser automation being supported in all languages while the testing ecosystem integration differs.

The cheapest way to size that gap is to try one documentation URL in two trees. playwright.dev/docs/test-ui-mode answers, and it documents UI mode, started with npx playwright test --ui. playwright.dev/python/docs/test-ui-mode returns 404, and so does the Java equivalent. A feature with no page in a language's documentation tree is not a feature that language has.

Take that exactly as far as it goes. UI mode is what we checked. The trace viewer and codegen both have Python and Java pages and both work there, and the Pytest plugin brings fixtures and tracing options of its own, so this is not a claim that the whole Playwright Test surface is missing outside Node. The narrower claim still decides things: a Java team is buying a browser automation library it drives from JUnit, and part of what it has been reading about belongs to somebody else's language. Firm86 delivers a suite in all four bindings, and the binding decides whether the runner comes with it. Settle that before the language gets picked.

An upgrade moves the browsers, and the CI image with them

Browser binaries belong to the Playwright version that installed them, so a version bump is never only a package bump. The Docker guide states the consequence in one sentence: "It is recommended to always pin your Docker image to a specific version if possible. If the Playwright version in your Docker image does not match the version in your project/tests, Playwright will be unable to locate browser executables."

In practice the image tag and package.json move on the same commit. A dependency bot that raises a pull request against one and leaves the other turns the pipeline red with an error about missing executables, which reads like a broken install rather than a version mismatch. The team that has never seen it before loses most of a morning to it.

Several hundred megabytes before the first test runs

Every machine that runs the suite keeps its own copies of the three browsers, under a cache folder in the user profile. The browsers documentation says they take a few hundred megabytes of disk when installed, and it prints its own du -hs output to show it: 281M, 187M and 180M against the Chromium, Firefox and WebKit directories.

Those figures are the documentation's example and not a measurement of your machine — the directory names in that listing have their version digits replaced with Xs, which is what the sample is telling you about itself. The size moves with every release. Where it lands does not: every developer laptop, every CI container, and every base image somebody has to keep small.

On Firefox, the documentation and the runtime disagree about isMobile

The browser.newContext() options table says of isMobile, the flag that makes the meta viewport tag count and turns touch events on, that it "is a part of device, so you don't actually need to set it manually. Defaults to false and is not supported in Firefox." On 1.62.1 the bundled Firefox 153.0 applies it anyway. Served a document asking for width=980 in a 412-pixel viewport, all three engines report a 980-pixel layout with isMobile set and 412 without it, which is the measurement that separates the two behaviours rather than a media query that would match either way.

Read that as a reason for caution rather than a reprieve. Not supported is a statement about what the vendor has promised, not about what the build does this week, and the two have no obligation to stay in step: a matrix that runs mobile descriptors on Firefox works today on something nobody has undertaken to keep working, and the release that changes it owes you no note. Treat the documented sentence as the contract and the observed behaviour as weather.

Component testing changed shape in 1.62

Component testing now runs on stories and a gallery page served by your own dev server, driven by the built-in fixtures.mount() fixture, and the guide says it replaces the experimental @playwright/experimental-ct-react and @playwright/experimental-ct-vue packages. A team that adopted those packages has migration work in front of it that nobody put in a plan. This is the general shape of adopting a framework that is still moving, and a capability taken up early is the part most likely to be replaced under you.

When is Playwright the wrong choice?

Hold your own situation against these. They are conditions, and any one of them is enough.

So is Playwright worth it?

The question needs a repository attached before it means anything: worth it for which application, written in which language, against which compatibility matrix. Answered in the abstract it is marketing in whichever direction the answerer prefers.

Attach those and the answer gets short. A web application, a matrix that Chromium, Firefox and WebKit cover, a team writing TypeScript or JavaScript, and a pipeline that already runs on every commit: Playwright takes away the waiting code, the assertion library, the driver binaries and the Grid, and hands back a failure somebody can step through. We would take that trade every time, which is what you would expect from a company that works in one framework.

Move any one of those and the answer moves with it. In Python or Java the library is excellent and the runner is somebody else's; with a native app in the product, half the test plan lives in another tool. A contract that names iOS Safari puts the devices before the framework.

None of that decides the part that costs the money: the suite itself, the pipeline definition around it, and the document your team writes the eleventh test from. That work costs about the same in any framework, and it is where we come in: building the suite.

Questions

What are the main disadvantages of Playwright?

It drives browsers, so it does not test native mobile apps, it is not a load-testing tool, and its WebKit build is neither branded Safari nor Safari on an iPhone. Internet Explorer is not on the supported list either. The one that catches teams out is the runner: the library has four official language bindings and the test runner that goes with it is the JavaScript and TypeScript half only, so Python, Java and .NET teams drive Playwright from pytest, JUnit or MSTest.

Can Playwright test mobile apps?

No. What Playwright offers for mobile is emulation: a device descriptor configures a browser it already ships with a phone's user agent, screen size, viewport, scale factor and touch flag, which covers responsive layout and touch behaviour on a mobile site. It does not reach a native UI. Android automation is real, and the API reference marks it experimental and scopes it to Chrome for Android and Android WebView, driven over ADB and still a browser. Native applications are Appium's category or a device cloud's, and Firm86 sells neither.

Does Playwright work with Safari?

It works with WebKit, which is a different answer. Playwright ships a WebKit build derived from the main branch, often ahead of what Apple Safari has shipped, and running it catches engine-level problems early. The browsers documentation says Playwright does not work with the branded version of Safari because it relies on patches. Safari on a real iPhone is a device question before it is a framework question, and Playwright does not answer it.

Can Playwright do load testing?

No. The unit of work is a browser context inside a real browser process, and concurrency comes from worker processes on the machines running the tests, so every simulated user costs a browser. There is no virtual-user model and no ramp profile in it. Load work belongs to a load-testing tool, and Firm86 does not sell a load-testing engagement.

Is Playwright only for JavaScript?

No. The library has four official bindings, each with its own Microsoft repository: JavaScript and TypeScript, Python, Java and .NET, and the documentation says all core browser automation features are supported in all of them. The test runner is the Node.js half only, and the documentation routes the rest elsewhere: the Pytest plugin for Python, any framework such as JUnit or TestNG for Java, and MSTest, NUnit, xUnit and xUnit v3 base classes for .NET. UI mode is the feature you can check for yourself, because it has a Node documentation page and the Python and Java equivalents of that URL return 404.

Is any part of the product a native app?

It is the first thing we ask, because Playwright does not drive one and the answer decides how much of your test plan it can reach. We would rather establish that on the first call than in month two. Send it, along with the browsers the application has to work in and the language your team writes tests in, and we come back with which of your flows a Playwright suite covers.