Comparison

Playwright vs Selenium: the comparison everyone runs skips one row

Search this matchup and you get the same article fifteen times: architecture, speed benchmarks, auto-wait, browser support, language bindings, adoption charts. All of it is true. None of it tells you about the row that actually shows up on your sprint board every two weeks. This page covers the standard comparison fast, then spends most of its length on the row the others leave out: who hand-writes the selector, and why switching framework does not change the answer.

Direct answer (verified 2026-05-16)

For a new web app in 2026, pick Playwright. It talks to the browser directly over the DevTools Protocol, auto-waits for elements so you stop writing explicit waits, and installs its browsers without a separate driver binary. Pick Selenium when your team is polyglot (Ruby, PHP, or a deep Java QA org), or when you depend on a Selenium Grid or legacy-browser matrix you cannot retire. One caveat that decides nothing in the table but everything in practice: neither framework removes selector maintenance. With both, a human writes and owns the locator string, and that string breaks when the UI changes. Sources: playwright.dev, selenium.dev.

M
Matthew Diakonov
8 min read

The framework war, settled in one screen

Here is the honest version of the standard comparison. Most of it is no longer a debate. Playwright drives the browser through the Chrome DevTools Protocol over one persistent WebSocket; Selenium drives it through the WebDriver protocol, historically a request and response per command. That gap is real, though Selenium 4 narrowed it by adding WebDriver BiDi, a WebSocket channel of its own. Playwright auto-waits for an element to be actionable before it acts; Selenium expects you to write the wait. Selenium reaches more languages and a wider browser matrix; Playwright covers the modern three from a single install. For greenfield web work that is enough to decide it. Read the last row carefully, because the rest of this page lives there.

Playwright vs Selenium, the rows every guide covers

FeatureSeleniumPlaywright
Browser communicationWebDriver protocol, historically one HTTP request per commandChrome DevTools Protocol over a single persistent WebSocket
Waiting for elementsExplicit waits you write (WebDriverWait, expected_conditions)Auto-waits for actionability before every action
Driver setupNeeds a matching browser driver (Selenium Manager now auto-resolves it)npx playwright install pulls the browsers, no separate driver
Language bindingsJava, Python, C#, Ruby, JavaScriptJavaScript/TypeScript, Python, Java, .NET
Cross-browser reachWidest, including legacy builds and Grid-distributed nodesChromium, Firefox, WebKit from one install
Who writes the selectorYou do (By.xpath, By.cssSelector, By.id)You do (getByRole, getByTestId, locator)

Selenium 4 added WebDriver BiDi, a WebSocket channel that narrows the protocol gap, so the speed row matters less than it did. The row that does not move is the last one: in both columns, a human still authors and maintains the selector.

The row no comparison table has: who writes the selector

Every test, in either framework, points at the page through a string a human typed. In Selenium it looks like By.xpath("//div[2]/form/button"). In Playwright it looks like getByRole('button', { name: 'Sign up' }). Playwright's version is genuinely better: it leans on the accessible role and name instead of a positional path, so it survives more changes. But it is still a string, written against the DOM and the accessible names that exist today, and committed to your repo. The day someone renames that button, the string is wrong. The framework cannot know the rename was harmless. The test fails, CI goes red, and a person has to go fix a string for a bug that never existed.

That is the part the comparison articles flatten into a single syntax footnote. Migrating from Selenium to Playwright upgrades the syntax and the waiting behavior. It does not change who is on the hook for the selector. Look at the three approaches side by side:

// Selenium 4 — you author an XPath against today's DOM shape
driver.findElement(
  By.xpath("//div[2]/form/button[contains(., 'Sign up')]")
).click();

// Playwright — cleaner syntax, still a string you own and commit
await page.getByRole('button', { name: 'Sign up' }).click();

// Assrt — there is no selector string to write or maintain
//   1. browser_snapshot()  ->  live a11y tree, every node gets [ref=eN]
//   2. browser_click({ element: 'Sign up button', ref: 'e12' })
//   ref=e12 is recomputed on the next snapshot, so nothing rots

The first two lines are the framework choice. The third is a different choice entirely: not a smarter selector, but no selector string at all. Before that makes sense, it helps to watch the cost the first two share.

The maintenance loop a framework choice does not break

This is the lifecycle of a single selector. It runs the same whether the string is an XPath or a Playwright locator. Watch it once and the recurring cost becomes obvious.

One selector, from green to red and back

01 / 05

Day 1

You write page.getByRole('button', { name: 'Sign up' }). It passes locally. You commit it with the feature.

Nothing in that loop is a Playwright problem or a Selenium problem. It is a hand-written-selector problem. Auto-wait, the headline Playwright feature, never enters the loop, because the failure was not a timing failure. The element was found instantly; it just was not the element the string described anymore.

0

Auto-wait fixed timing flake. It never touched selector flake. The only way to retire selector maintenance is to stop hand-writing the selector in the first place.

From the Assrt agent execution loop, agent.ts

What Assrt does instead, traced through the actual code

Assrt is not a third framework competing with Playwright and Selenium. It runs on Playwright: its package.json declares @playwright/mcp ^0.0.70 as a hard dependency. What it changes is the selector layer. Instead of you writing a locator string, the agent works off the live accessibility tree.

The Assrt step loop: snapshot, pick a ref, act, re-snapshot

1

browser_snapshot

Pull the live accessibility tree. Every interactive node is tagged with a fresh [ref=eN] computed from the current page.

2

Agent picks a ref

The model reads the tree, matches the element by role and visible name, and selects ref=e12. No string is saved.

3

browser_click

Playwright MCP executes the real click against Chromium using that ref. Identical engine to a hand-written Playwright test.

4

Re-snapshot

If the page changed and the ref went stale, the next snapshot just returns the current refs. There is no committed selector to repair.

Anchor fact, verifiable in the source

Open assrt-mcp/src/core/agent.ts and read the system prompt around lines 213 to 218. It has a section titled, word for word, "Selector Strategy (Playwright MCP refs)". The procedure is: call snapshot to get the accessibility tree, find the element, use its ref value like ref="e5". Step 5 is the self-healing rule, verbatim: "If a ref is stale (action fails), call snapshot again to get fresh refs." Line 236 is even more explicit: "Do NOT use ref attributes as DOM selectors (they don't exist in the DOM)." The refs are not committed anywhere. There is no By.xpath, no By.cssSelector, and no page.locator("...") string anywhere in the execution loop. The browser methods in browser.ts confirm it: they call browser_snapshot and browser_click with a ref, never a hand-authored path.

That is the whole point. A ref like e12 is recomputed from the page on the next snapshot. When the button is renamed, the next snapshot just returns whatever is on the page now, and the agent re-matches the element by its role and visible name. There is no string in your repo to go red, because there is no string in your repo. The maintenance loop from the previous section has nowhere to start.

When Selenium is still the right call, honestly

A comparison that only ever points one direction is marketing, not analysis. There are real cases where Selenium beats Playwright and where neither one of them, plus an agent layer, is the move.

Stay on Selenium if your test code is written in a language Playwright does not bind well, Ruby being the clearest example, or if your CI is built around a Selenium Grid with device and legacy-browser coverage you are contractually on the hook for. The cost of rewriting a large, stable Selenium suite almost always exceeds the ergonomic gain, and a quiet suite is not a problem that needs solving.

Pick Playwright for greenfield JavaScript or TypeScript work. The architecture is better, auto-wait removes a whole genre of flake, and the debugging tools (trace viewer, the inspector) are excellent. This is the default for most teams starting a suite in 2026, and it is the engine Assrt runs on.

Add Assrt when the bottleneck is not the framework but the hours. If your team is choosing Playwright because the selectors will be more durable, that is true and also not enough: durable is not the same as free. Assrt removes the selector-authoring step entirely, keeps the engine as standard Playwright, and stays open source under the MIT license so the tests run in your own CI with no cloud in the path. It is the answer to the row the comparison tables never print.

Bring a real flow and we will run it without a single selector

Thirty minutes: pick a flow from your app, watch Assrt drive it through Playwright off the accessibility tree, and see the artifacts land on disk.

Frequently asked questions

Is Playwright actually faster than Selenium?

In most independent benchmarks, yes, at the wall-clock-per-suite level. The structural reason is the transport. Selenium drives the browser through the WebDriver protocol, historically one HTTP request and response per command, with a driver process in the middle. Playwright holds a single persistent connection over the Chrome DevTools Protocol and pipelines commands, so the per-action overhead is lower. That said, raw protocol speed is rarely the thing that decides how long your CI run takes. Parallel sharding, how aggressively you wait, and how often a flaky selector forces a rerun usually dominate. Selenium 4 also added WebDriver BiDi, a WebSocket channel that narrows the protocol gap, so 'Selenium is slow' is less true than it was in 2020. Treat speed as a real but secondary input.

Does Selenium support more browsers and languages than Playwright?

Languages: Selenium ships official bindings for Java, Python, C#, Ruby, and JavaScript. Playwright ships JavaScript/TypeScript, Python, Java, and .NET. If your QA org writes in Ruby, Selenium is the honest answer. Browsers: Playwright drives Chromium, Firefox, and WebKit out of the box from one install. Selenium, through the WebDriver protocol and Selenium Grid, reaches a wider matrix including legacy versions and distributed device fleets. If you are a modern web app targeting current browsers, Playwright's matrix is enough. If you are contractually testing old enterprise browser builds across a Grid, Selenium still has reach Playwright does not.

Should I migrate an existing Selenium suite to Playwright?

Not reflexively. A working Selenium suite has value: it encodes real product knowledge and it is paid for. Migrating means rewriting every test, re-debugging the flaky ones, and retraining the team, all before you ship a single new feature. The migration pays off when your Selenium suite is already a maintenance sink, when the team is fighting explicit waits weekly, or when you are adding so many new flows that greenfield Playwright would dwarf the legacy suite anyway. If your Selenium suite is small, stable, and quiet, leave it. We wrote a separate on-ramp for the cases where migration does make sense.

If Playwright auto-waits, why do Playwright tests still go flaky?

Auto-wait solves one class of flake: timing. Before an action, Playwright checks that the element is attached, visible, stable, and able to receive events, so you stop writing sleep() and explicit waits. It does not solve the other class: selector flake. The locator string you wrote, getByRole('button', { name: 'Sign up' }) or page.locator('.cta-primary'), is matched against a specific DOM and a specific set of accessible names. When a designer renames the button, restructures a wrapper, or swaps a class, the string stops matching and the test fails even though the app works. Auto-wait never touches that. Switching from Selenium to Playwright upgrades the syntax of the selector and the waiting behavior. It does not change the fact that you own the selector and the selector rots.

What does Assrt use instead of XPath or CSS selectors?

Accessibility-tree refs. On every step, Assrt calls browser_snapshot, which returns the live accessibility tree with each interactive node tagged by a fresh reference like ref="e12". The agent reads that tree, matches the element by its role and visible name, and calls browser_click or browser_type with that ref. Nothing is committed to a file. The system prompt in assrt-mcp/src/core/agent.ts has a section literally titled "Selector Strategy (Playwright MCP refs)", and its rule for a changed page is plain: "If a ref is stale (action fails), call snapshot again to get fresh refs." There is no By.xpath, no By.cssSelector, and no page.locator("...") string anywhere in the execution loop. There is no selector string to rot because there is no selector string.

Is Assrt a replacement for Playwright, or does it use Playwright?

It uses Playwright. Assrt is not a fourth browser-automation framework competing with Playwright and Selenium. Its package.json declares @playwright/mcp ^0.0.70 as a hard dependency, and the runtime spawns it as a child process on startup. Every click and type is a real Playwright action against a real browser. Assrt is the agent layer that decides what to click, sitting on top of the same Playwright engine Microsoft ships. So the framework question and the Assrt question are not the same question. You pick Playwright over Selenium for the framework. You add Assrt when you want to stop hand-authoring the selectors that Playwright, by itself, still expects you to write.

Is Assrt open source, and does it lock me into a cloud?

It is open source under the MIT license, at github.com/assrt-ai/assrt-mcp, and it runs locally. It drives a browser on your machine or your CI runner, writes results and video to local files, and keeps scenario plans as plain Markdown in your repo. There is no proprietary YAML format and no cloud you have to route tests through. That is the deliberate contrast with closed test-automation SaaS that can cost thousands of dollars a month and holds your test definitions inside their product. If you stop using Assrt, you keep your Markdown plans, and the underlying engine is still standard Playwright.

assrtOpen-source AI testing framework
© 2026 Assrt. MIT License.

How did this page land for you?

React to reveal totals

Comments ()

Leave a comment to see what others are saying.

Public and anonymous. No signup.