Playwright vs Cypress in 2026: Same Test, Run in Both

Playwright vs Cypress comes down to your requirements. Pick Playwright for a new end-to-end suite that must cover Chromium, Firefox and WebKit, run in parallel without a hosted service, drive two users in one test, or be written in Python, Java or .NET. Pick Cypress if your team writes JavaScript, debugs by watching each command run against the live app, and wants official component mounts. We ran the same test in both, and the logs are below.
Qodex writes these scenarios for you, runs them on every pull request, and saves them as standard Playwright and HTTP code you own. See Qodex UI testing or try it on your app.
Checked with Playwright 1.64.1 and Cypress 16.2.0 on Node.js 26, read 23 October 2026. Every file in the side-by-side section ran as printed in an empty folder, and every output block is copied from that run's log. Each fact about either tool links to its own documentation, read the same day.
Playwright vs Cypress: the decision table
Find the row that matches a requirement you actually have. One hard requirement settles the choice; the other rows are preferences.
| If you need | Pick | Why |
|---|---|---|
| Firefox and WebKit as well as Chrome, in CI | Playwright | It runs Chromium, Firefox and WebKit; Cypress lists WebKit as experimental |
| Tests written in Python, Java or .NET | Playwright | Cypress tests are JavaScript or TypeScript |
| Two users, two browsers or a second tab in one test | Playwright | Cypress cannot drive two browsers at once, and its tab plugin is Chromium only |
| Parallel runs with no hosted service | Playwright | Test files run in parallel worker processes; Cypress's built-in parallelization runs through Cypress Cloud |
| To watch each command against the live app while you write | Cypress | Its runner keeps a snapshot of the app at every command |
| Official component mounts for React, Angular, Vue or Svelte | Cypress | Playwright tests components through a story gallery you build |
| An existing Cypress suite that already catches bugs | Cypress | Moving means rewriting every spec |
Every row comes from the tools' own pages, read 23 October 2026. For Playwright: the browsers, languages, browser contexts and parallelism pages. For Cypress: the launching browsers, trade-offs, parallelization and component testing pages, plus its homepage for Time Travel.
What each tool is
Playwright

Playwright is Microsoft's open-source browser automation library, licensed Apache-2.0, with its own test runner, Playwright Test. The runner starts worker processes, and each worker starts its own browser. Bindings exist for JavaScript and TypeScript, Python, Java and .NET. Version 1.64.1 reached npm on 14 October 2026.
Cypress

Cypress is an open-source testing app, licensed MIT, that runs end-to-end and component tests inside the browser next to your app. Cypress Cloud is the hosted service around it for recorded runs, parallelization and Test Replay, with a free Starter tier. Its changelog dates version 16.2.0 to 13 October 2026.
The same test in Playwright and Cypress
A comparison table describes syntax. This section runs it. The app is a one-page task list that saves through an API with a 400 ms delay, so both tools have to wait for a real network call. Each tool gets two tests: the happy path, and a server error that the test stubs.
Set up the folder
Start in an empty folder. npm 11.20 skipped Cypress's install script in our run, so the list ends with a command that downloads the Cypress binary when it is missing.
mkdir playwright-vs-cypress-demo
cd playwright-vs-cypress-demo
npm init -y
npm install -D @playwright/test@1.64.1 cypress@16.2.0
npx playwright install chromium firefox webkit
npx cypress install
mkdir -p public tests cypress/e2e
The folder ends up with six files of your own, all printed below: server.js, public/index.html, playwright.config.js, tests/tasks.spec.js, cypress.config.js and cypress/e2e/tasks.cy.js.
The app under test
A Node server with no dependencies. It serves the page and answers POST /api/tasks after 400 ms.
Save this as server.js:
const http = require('node:http');
const fs = require('node:fs');
const page = fs.readFileSync(__dirname + '/public/index.html');
http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/api/tasks') {
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', () => {
let title;
try {
title = String(JSON.parse(body).title);
} catch {
res.writeHead(400);
return res.end();
}
// A 400 ms delay stands in for a slow backend.
setTimeout(() => {
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 1, title }));
}, 400);
});
return;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(page);
}).listen(4173, () => console.log('App on http://localhost:4173'));
Save this as public/index.html:
<!doctype html>
<html>
<body>
<h1>Tasks</h1>
<label>New task <input id="title" /></label>
<button id="add">Add task</button>
<output id="status"></output>
<ul id="list"></ul>
<script>
document.getElementById('add').addEventListener('click', async () => {
const title = document.getElementById('title').value;
const res = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
const status = document.getElementById('status');
if (!res.ok) {
status.textContent = 'Could not save the task';
return;
}
const task = await res.json();
const li = document.createElement('li');
li.textContent = task.title;
document.getElementById('list').append(li);
status.textContent = 'Saved';
});
</script>
</body>
</html>
Start it in its own terminal and leave it running:
$ node server.js
App on http://localhost:4173
The Playwright test
Save this as playwright.config.js:
const { defineConfig } = require('@playwright/test');
module.exports = defineConfig({
testDir: './tests',
use: { baseURL: 'http://localhost:4173' },
});
Save this as tests/tasks.spec.js:
const { test, expect } = require('@playwright/test');
test('adds a task', async ({ page }) => {
await page.goto('/');
await page.getByRole('textbox', { name: 'New task' }).fill('Write the release notes');
await page.getByRole('button', { name: 'Add task' }).click();
await expect(page.getByRole('status')).toHaveText('Saved');
await expect(page.getByRole('listitem')).toHaveText('Write the release notes');
});
test('shows an error when the save fails', async ({ page }) => {
await page.route('**/api/tasks', (route) => route.fulfill({ status: 500 }));
await page.goto('/');
await page.getByRole('textbox', { name: 'New task' }).fill('Write the release notes');
await page.getByRole('button', { name: 'Add task' }).click();
await expect(page.getByRole('status')).toHaveText('Could not save the task');
});
Run it in a second terminal:
$ npx playwright test
Running 2 tests using 1 worker
✓ 1 tests/tasks.spec.js:3:1 › adds a task (969ms)
✓ 2 tests/tasks.spec.js:11:1 › shows an error when the save fails (109ms)
2 passed (1.5s)
Both tests sit in one file, and Playwright runs the tests of one file in order in one worker, which is what the first line reports. The same spec ran in all three engines with one flag, because this config defines no projects:
$ npx playwright test --browser=all
Running 6 tests using 3 workers
✓ 1 [chromium] › tests/tasks.spec.js:3:1 › adds a task (887ms)
✓ 4 [chromium] › tests/tasks.spec.js:11:1 › shows an error when the save fails (99ms)
✓ 2 [webkit] › tests/tasks.spec.js:3:1 › adds a task (1.2s)
✓ 5 [webkit] › tests/tasks.spec.js:11:1 › shows an error when the save fails (192ms)
✓ 3 [firefox] › tests/tasks.spec.js:3:1 › adds a task (1.4s)
✓ 6 [firefox] › tests/tasks.spec.js:11:1 › shows an error when the save fails (179ms)
6 passed (2.4s)
The Cypress test
Save this as cypress.config.js:
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: { baseUrl: 'http://localhost:4173', supportFile: false },
});
Save this as cypress/e2e/tasks.cy.js:
describe('tasks', () => {
it('adds a task', () => {
cy.visit('/');
cy.getByRole('textbox', { name: 'New task' }).type('Write the release notes');
cy.getByRole('button', { name: 'Add task' }).click();
cy.getByRole('status').should('have.text', 'Saved');
cy.getByRole('listitem').should('have.text', 'Write the release notes');
});
it('shows an error when the save fails', () => {
cy.intercept('POST', '/api/tasks', { statusCode: 500 });
cy.visit('/');
cy.getByRole('textbox', { name: 'New task' }).type('Write the release notes');
cy.getByRole('button', { name: 'Add task' }).click();
cy.getByRole('status').should('have.text', 'Could not save the task');
});
});
Run it against the installed Google Chrome:
$ npx cypress run --browser chrome
Running: tasks.cy.js (1 of 1)
tasks
✓ adds a task (628ms)
✓ shows an error when the save fails (224ms)
2 passing (891ms)
That is an excerpt; the full log adds a results table. Its header reported Cypress 16.2.0 on Chrome 155 (headless). Without --browser chrome, Cypress picked Electron 146 (headless) and printed a warning that Electron is deprecated as a test browser.
What the run showed
The locators now match. Cypress 16.2.0 added cy.getByRole(), which finds elements by ARIA role and accessible name (changelog, read 23 October 2026). Both specs above find the textbox, the button, the status and the list item the same way. On Cypress 16.1 or older, those lines need cy.get() or cy.contains() instead. For the Playwright side, see Playwright getByRole.
Neither test waits by hand. The save takes 400 ms, and no line in either spec sleeps or polls. Playwright's expect and Cypress's should both retry until the text appears or a timeout runs out.
The failures read differently. We changed the expected status to Saved! in both specs and ran them again. Playwright gave up after 5,000 ms and printed a call log of what the locator found on each retry:
Error: expect(locator).toHaveText(expected) failed
Locator: getByRole('status')
Expected: "Saved!"
Received: "Saved"
Timeout: 5000ms
Call log:
- Expect "toHaveText" getByRole('status') with timeout 5000ms
- waiting for getByRole('status')
5 × locator resolved to <output id="status"></output>
- unexpected value ""
9 × locator resolved to <output id="status">Saved</output>
- unexpected value "Saved"
Cypress gave up after 4,000 ms, printed a diff and saved a screenshot of the failed test:
1) tasks
adds a task:
Timed out retrying after 4000ms
+ expected - actual
-'Saved'
+'Saved!'
at Context.eval (webpack://playwright-vs-cypress-demo/./cypress/e2e/tasks.cy.js:6:27)
Those two timeouts are the defaults each tool used in our run, with no timeout set in either config.
Stubbing is one line in each. page.route() with route.fulfill() and cy.intercept() with a static response both replaced the API with a 500 before the page loaded. The durations in the logs come from one laptop run, so they are not a benchmark.
Playwright vs Cypress feature by feature
| Factor | Playwright 1.64.1 | Cypress 16.2.0 |
|---|---|---|
| Released | 14 October 2026, on npm | 13 October 2026, per the changelog |
| Test languages | JavaScript, TypeScript, Python, Java, .NET | JavaScript, TypeScript |
| Browsers | Chromium, Firefox, WebKit, branded Chrome and Edge | Chrome family and Firefox; WebKit experimental; Electron deprecated |
| Two users or tabs in one test | Several pages and browser contexts per test | One browser; tabs through @cypress/puppeteer, Chromium only |
| Parallel runs | Worker processes on one machine, by default | Spec files across CI machines, orchestrated by Cypress Cloud |
| Test retries by default | 0 | 0 |
| Assertion retry in our run | 5,000 ms, then fail | 4,000 ms, then fail |
| Find by role | getByRole() | cy.getByRole(), new in 16.2.0 |
| Network stubs | page.route() | cy.intercept() |
| Component tests | Built-in mount fixture and a story gallery you own | Official mounts for React, Angular, Vue and Svelte |
| Node.js | 22, 24 or 26 | 22, 24, or 26 and later |
| Licence | Apache-2.0 | MIT |
Release dates come from the npm registry and the Cypress changelog. Node.js support comes from Playwright's installation page and Cypress's install guide. Retry defaults come from the Playwright and Cypress retry guides. All were read 23 October 2026.
Browsers, languages and tabs
Playwright's browsers page says it "can run tests on Chromium, WebKit and Firefox browsers as well as branded browsers such as Google Chrome and Microsoft Edge." Cypress's launching-browsers page says it "currently supports Chrome-family browsers (including Edge and Chrome for Testing) and Firefox, plus experimental WebKit." It adds that Cypress "officially supports the latest 3 major versions of Chrome, Firefox, and Edge", and it marks Electron as deprecated.
Playwright's mobile support is emulation. It ships a registry of device parameters that can simulate "user agent, screen size, viewport and if it has touch enabled" (emulation guide). That catches layout and touch problems in a desktop engine, which is a narrower claim than testing on a phone.
Language support is the clearest split. Playwright's languages page covers JavaScript and TypeScript, Python with a recommended pytest plugin, Java with any framework such as JUnit or TestNG, and .NET with MSTest, NUnit and xUnit base classes. Cypress's trade-offs page puts its own position plainly: "The only language we'll ever support is the language of the web: JavaScript." Its specs can be TypeScript too.
The same trade-offs page sets the tab limit: "You cannot use Cypress to drive two browsers at the same time." It continues: "You can test multiple tabs, however, using our @cypress/puppeteer plugin." That plugin's readme limits it to Chromium-family browsers. Calling it in Firefox or WebKit fails with an error. Moving between two origins in one test goes through cy.origin(), which since Cypress 14 is required between any two origins (cy.origin).
In Playwright, "each browser context can host multiple pages (tabs)", and one test can create several isolated contexts. An admin approving what a customer just submitted, two people in one chat, or a payment popup all fit in one test. If Selenium or Puppeteer is also on your list, see Playwright vs Selenium and Playwright vs Puppeteer.
Debugging, waiting and retries
Cypress's debugging centres on watching the run. Its homepage describes Time Travel: "Hover any command to see your app at that exact step." For a failure in CI, Test Replay in Cypress Cloud plays the run back, and the pricing page lists it on the free Starter tier.
Playwright's debugging centres on the record of a finished run. Its Trace Viewer "is a GUI tool that helps you explore recorded Playwright traces after the script has run" (Trace Viewer). You download the trace from the CI job and open it on your machine, with no hosted service involved. The call log in the failure above is the terminal version of the same idea.
Retries need exact wording, because two different things share the name. Assertion retries are built in to both tools, as the failure logs showed. Whole-test retries are off by default in both: Playwright says "By default failing tests are not retried", and Cypress's retry guide sets both run modes to 0. Turning them on hides a flaky test without fixing it. For the assertion side in detail, see Playwright assertions. For explicit waits, see Playwright waitForSelector.
Parallel runs and what CI costs
Playwright parallelizes on one machine. Its parallelism guide says: "By default, test files are run in parallel. Tests in a single file are run in order, in the same worker process." Each worker is an OS process with its own browser. To spread a suite across several CI machines, you shard it, with no service in between.
Cypress parallelizes across machines through its service. The parallelization guide says "Running tests in parallel requires the --record flag be passed", and Cypress Cloud then balances spec files across your CI machines. The unit of work is a whole spec file, so the balance depends on how you split your specs.
Cypress Cloud prices, from cypress.io/pricing, read 23 October 2026, billed annually:
| Plan | Price | Users | Recorded test results |
|---|---|---|---|
| Starter | Free | 10 | 500 a month |
| Team | From $67 a month, billed at $799 a year | 50 | 120k a year |
| Business | From $267 a month, billed at $3,199 a year | 50 | 120k a year |
| Enterprise | Custom | Unlimited | 1.8M a year |
The Starter tier includes Parallelization and Test Replay, so a free account can run parallel specs within its 500 results. Playwright needs no hosted service to run in parallel, so its cost is the machines that run it. Neither project publishes a speed comparison we could verify. To get a number you can defend, time your own critical flows on your own CI runner with the worker count you would use.
Component testing and API calls
Cypress has the clearer component story today. It "currently has official mounting libraries for React, Angular, Vue, and Svelte" (component testing, read 23 October 2026). The supported pairs are React 18 to 19 with Vite 8 or Webpack 5, Next.js 15.0.4 or later and 16, Vue 3, and Angular 21 to 22 on Webpack 5. Svelte 5 is listed as alpha.
Playwright changed its approach in version 1.62. A component test "is a regular Playwright end-to-end test that runs against a small story gallery page served by your own dev server." The experimental @playwright/experimental-ct-* packages "have been removed and are no longer published" (components guide, read 23 October 2026). You own the gallery page, and Playwright ships its setup as an agent skill. Playwright component testing walks through it.
For API calls inside a UI test, both tools have a built-in route. Playwright's request fixture "respects configuration options like baseURL or extraHTTPHeaders" (API testing guide). Cypress's cy.request() sends the request from Node.js, and cy.task() runs code in Node.js for jobs such as seeding a database (cy.request, cy.task). For the Playwright side in depth, see Playwright API testing.
AI test authoring in both tools
Both projects now ship AI authoring. Playwright documents three agents: a planner, a generator and a healer. Its test agents page says the "Generator agent uses the Markdown plan to produce executable Playwright Tests." Cypress's homepage offers to "Generate self-healing tests with cy.prompt". Both were read 23 October 2026.
Either way, the output is a spec in the tool's own format, and the decision table above still decides which format you want to own. To record a flow instead of prompting for one, see Playwright codegen.
Moving from Cypress to Playwright
Migrate for a requirement Cypress blocks or a cost you have measured, not for a benchmark from someone else's suite. The two specs above show what a port looks like at small scale. The structure carried over line for line, and every command still changed.
| In your Cypress suite | What carries over | What you rewrite |
|---|---|---|
describe and it blocks | Test names and structure | it becomes test, and every step is awaited |
Selectors and cy.getByRole() | Roles, names and test ids | Chained queries become locators |
should() assertions | What each assertion checks | Each becomes an expect() matcher |
cy.intercept() stubs | URL patterns and stub bodies | Each becomes page.route() |
cy.request() and cy.task() setup | Endpoints and seed logic | It moves into the request fixture or a fixture file |
| Custom commands | The logic inside them | Each becomes a fixture or a helper function |
| Component tests | The component and its props | Each needs a story in the gallery |
| Cypress Cloud history and Test Replay | Nothing | Traces and the HTML report replace them |
Run it as a measured change. Port your critical flows first and time both the authoring and the CI run. Keep Cypress as the release gate for one release while Playwright reports alongside it, then switch the gate and delete the old suite. Custom commands map to fixtures; Playwright fixtures shows the pattern. If you are still comparing options, Cypress alternatives and Playwright alternatives list the rest of the field.
Conclusion
Playwright is the default for a new suite that needs WebKit and Firefox, a language other than JavaScript, two users in one test, or parallel runs with no hosted service. Cypress fits a JavaScript team that debugs by watching the runner, tests components through official mounts, or already has a suite that catches real bugs. With cy.getByRole() in Cypress 16.2.0, the test code itself now looks closer than the tools' architectures are. For the wider picture, see our UI testing guide.
Frequently Asked Questions
Is Playwright better than Cypress?
For a new suite with cross-browser, multi-language or multi-user needs, Playwright fits better: it runs Chromium, Firefox and WebKit, has Python, Java and .NET bindings, and runs files in parallel by default. Cypress fits better for a JavaScript team that wants its interactive runner and official component mounts. Your requirements decide it, not a ranking.
Is Playwright faster than Cypress?
There is no universal answer we could verify. Playwright runs test files in parallel worker processes on one machine by default. Cypress spreads spec files across CI machines through Cypress Cloud. Total time therefore depends on how many workers and machines you run. Time your own flows on your own CI runner.
Does Cypress support Safari or WebKit?
Cypress supports Chrome-family browsers and Firefox, and lists WebKit as experimental, according to its launching-browsers page read 23 October 2026. Playwright runs WebKit as one of its three engines, and the spec on this page passed in it.
Can Cypress test multiple tabs?
Yes, through the @cypress/puppeteer plugin, which works in Chromium-family browsers only. Cypress cannot drive two browsers at the same time, per its trade-offs page. Playwright opens several pages and isolated browser contexts in one test without a plugin.
Do I need a paid plan to run Cypress in parallel?
No. Parallel runs need Cypress Cloud recording, and the free Starter tier lists Parallelization, with 10 users and 500 test results a month, read 23 October 2026. Playwright runs test files in parallel locally and in CI with no service at all.
Does Cypress have getByRole now?
Yes. Cypress 16.2.0, released 13 October 2026, added cy.getByRole(), which finds elements by ARIA role and accessible name. Our Cypress spec above uses it for every element, and it passed in Chrome.
Can Playwright and Cypress run side by side during a migration?
Yes. They are separate npm packages with separate config files, and the example on this page runs both from one folder against one app. Keep the Cypress suite as the release gate while Playwright covers the same flows, then switch the gate once both agree for a full release.


