Cypress to Playwright migration: A step-by-step guide

Step by step guide to migrate Cypress tests to Playwright, compare architectures, map commands, reduce flakiness, debug faster with traces, and CI speed.

I like how every migration tells a story: a story of evolution, where teams move from comfort to capability. Cypress brought front-end testing within reach, but Playwright pushed it further with performance, scalability, and cross-browser reach.

Whether you're pursuing fast CI runs, reduced flakiness, or a better understanding of failures, this step-by-step guide will help you turn your test suite into a modern, robust automation framework.

Each year brings new frameworks, improved browser coverage, and more advanced debugging tools. Yet one question keeps coming up among QA teams: "Should we migrate from Cypress to Playwright?" If you've ever wondered that, this guide is for you. We'll walk through the Cypress to Playwright migration journey: from understanding their architectures to mapping commands, handling waits, and improving reporting with TestDino.

Prefer a reference you can keep open while you work? We maintain an open-source migration skill with the full command map and gotchas: Cypress-to-Playwright migration skill.

Overview

Cypress architecture

Cypress is known for its simplicity and developer-friendly syntax. It runs directly inside the browser, which makes debugging easy but also limits flexibility.

  • Single-browser-tab execution is great for speed, but it restricts multi-tab tests.
  • Network-layer control: it intercepts requests inside the browser.
  • Tight coupling with Chrome-based browsers.

This architecture makes Cypress excellent for small to medium-sized front-end tests but less ideal for cross-browser or large-scale automation setups. For a fuller side-by-side, see our Playwright vs Cypress comparison.

Playwright architecture

Playwright uses a browser-driver model similar to Selenium but with modern APIs and auto-waiting mechanisms. It's designed for multi-browser, multi-context testing at scale.

  • Supports Chromium, WebKit, and Firefox.
  • Cross-language support: JavaScript, TypeScript, Python, C#, and Java.
  • Parallel execution: runs multiple tests across browsers.
  • Powerful debugging tools: trace viewer and network monitoring.

Why teams are moving to Playwright

Most teams begin with Cypress due to its ease of use and rapid setup, but as projects expand, they hit the limits of scalability and compatibility. That's where Playwright comes into its own, letting teams push past those boundaries and evolve their frontend automation.

The numbers behind the shift (2026)

  • Playwright pulls ~54M weekly npm downloads vs Cypress's ~6.7M, roughly an 8× gap.

  • Playwright natively supports Chromium, Firefox, and WebKit; Cypress's Firefox support has been experimental since 2020, with no WebKit/Safari support.
  • One team reported a ~70% drop in execution time after migrating 200+ tests.

Signs you should move from Cypress to Playwright

  • Need for cross-browser or cross-device testing: Playwright supports Chromium, Firefox, and WebKit, enabling true cross-browser testing.
  • Struggling with parallel execution: large suites run faster in Playwright thanks to built-in parallelism.
  • Want headless, faster CI/CD runs: Playwright optimizes test execution in pipelines.
  • Need advanced network mocking and debugging: auto-waits, request interception, and the trace viewer simplify debugging.
  • Scaling automation across microservices: Playwright's multi-context architecture handles complex workflows efficiently.

If one or more of these signs sounds familiar, it may be time for a Cypress to Playwright migration. If flakiness is your main driver, see our guide to reducing flaky tests.

Key differences: Cypress vs Playwright

Feature Cypress Playwright
Browser support Chromium only, partial Firefox Chromium, WebKit, Firefox
Parallel execution Limited, slower in CI/CD Native parallelism, fast CI/CD
Test isolation Single tab, shared state can cause flakiness Independent browser contexts, better isolation
Network stubbing/mocking Basic support Advanced, flexible interception and custom mocking
Language support JavaScript/TypeScript JavaScript, TypeScript, Python, C#, Java
Debugging Built-in GUI, interactive CLI + trace viewer, step-through, inspect req/res
Flakiness handling Manual waits required Auto-wait built in, configurable retries
CI/CD integration Works but requires setup Native, optimized for pipelines
Cross-device testing Limited emulation Real devices, mobile emulation (iOS/Android)
Screenshots/video Automatic for failed tests Screenshots, video, detailed traces
Test reporting Basic, plugins for advanced Built-in detailed reporting, customizable
Community & ecosystem Large and mature, many plugins Growing ecosystem, first-class Playwright tools
Performance Slower with parallel tests Fast, optimized for multiple contexts
API testing Limited via workarounds Native API testing alongside UI
Auto-wait for elements Manual waits needed Auto-waits for ready elements
Learning curve Beginner-friendly, simpler API Slightly steeper, more flexible and powerful

This comparison shows why the Cypress to Playwright migration has become a trend among modern QA teams.

The 5 mindset shifts that make migration click

Most failed Cypress-to-Playwright migrations aren't syntax problems; they're mental-model problems. Get these five shifts and the rest is mechanical.

1. Command chains → async/await

Cypress commands are enqueued and run in a serial chain that looks synchronous but isn't. Playwright uses standard async/await, so you get native if/else, for loops, and try/catch back, with no cy.then() workarounds.

2. Whole-chain retry → lazy locators with auto-wait

Cypress retries the entire command chain until assertions pass or time out. Playwright locators are lazy: they do nothing until an action or assertion runs, at which point auto-waiting handles actionability for that one step.

3. In-browser → Node.js

Cypress test code runs inside the browser. Playwright runs in Node.js and drives browsers over a dev-tools protocol. You gain native filesystem/DB access; you lose direct window/document access (use page.evaluate() when you truly need it).

4. Single tab → multi-tab, multi-context, multi-browser

Playwright handles multiple pages, browser contexts, and engines (Chromium, Firefox, WebKit) in one test, including popups, OAuth flows, and multi-user scenarios. Cypress is one tab.

5. Custom commands → fixtures

Cypress extends behavior via Cypress.Commands.add(), a global mutable registry. Playwright uses test.extend() fixtures with dependency injection, giving you guaranteed teardown and full type safety.

Is Playwright the right choice for your testing needs?

Playwright is an excellent choice if you want to future-proof your test stack and automate easily across browsers, devices, and teams. It provides speed, reliability, and flexibility, all critical to large-scale QA operations. It's especially good when you need:

  • End-to-end coverage on Chrome, Safari, and Firefox: Playwright's native multi-browser support validates across top browsers with fewer environment-specific failures.
  • Fast headless execution for CI/CD: Playwright runs tests headlessly with low overhead, giving quicker feedback loops and deeper DevOps integration.
  • Deep debugging for flaky tests: trace viewer, network logs, and video capture make root-cause analysis fast. You can visually replay test steps and catch problems that otherwise pass undetected.

Step-by-step guide to Cypress to Playwright migration

This is the nine-stage process we recommend. You can keep CI green throughout, because Cypress and Playwright coexist until the final step.

1. Set up Playwright

Install Playwright in your project directory using the recommended initializer:

terminal
npm init playwright@latest

Or manually:

terminal
npm install -D @playwright/test
npx playwright install

Then add a test script to your package.json for easy execution:

package.json
"scripts": {
  "test:e2e""npx playwright test"
}

This sets up Playwright alongside your existing Cypress tests, preparing your project for a smooth migration.

2. Configure playwright.config (map your Cypress settings)

Port your Cypress config keys to their Playwright equivalents:

Cypress setting Playwright equivalent
baseUrl use: { baseURL }
defaultCommandTimeout expect: { timeout }
requestTimeout test-level timeout
setupNodeEvents globalSetup / globalTeardown
Cypress.env() process.env or use: {}

3. Create a basic Playwright test

example.spec.js
//  Playwright basic navigation test
import { test, expect } from '@playwright/test';

test('navigate to example.com'async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
});

This mirrors Cypress's it() syntax but with async/await for stability and modern automation practices.

4. Convert custom commands to fixtures

Replace Cypress.Commands.add() with test.extend() fixtures, grouping logic into page objects and API helpers that get guaranteed teardown:

fixtures.ts
import { test as base } from '@playwright/test';
import { TodosPage } from './TodosPage';

export const test = base.extend<{ todosPage: TodosPage }>({
  todosPage: async ({ page }, use) => {
    await page.goto('/todos');
    await use(new TodosPage(page));
  },
});

// In tests, the fixture is injected by name
test('adds todos'async ({ todosPage }) => {
  await todosPage.createTodo('Buy milk');
  await expect(todosPage.todos).toHaveCount(1);
});

5. Set up authentication with storageState

One of the biggest CI speedups: stop logging in on every test. Move from cy.session() to a setup project that logs in once and saves storageState to a file the tests reuse.

auth.setup.ts
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';

setup('authenticate'async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('admin@example.com');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.context().storageState({ path: authFile });
});

// Tests reuse the saved session, no login code needed
test('shows admin content'async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page.getByTestId('admin-panel')).toBeVisible();
});

6. Map Cypress commands to Playwright

This is the full command map. Prefer semantic locators (getByRole, getByLabel, getByText) over raw CSS where you can.

Cypress Playwright What changed
cy.visit('/path') await page.goto('/path') Explicit async/await
cy.get('sel') page.locator('sel') Lazy evaluation
cy.contains('text') page.getByText('text') Prefer semantic locators
cy.get('sel').click() await locator.click() Auto-waits for actionability
cy.get('sel').type('txt') await locator.fill('txt') Instant value set (pressSequentially for keystrokes)
cy.get('#email') page.getByLabel('Email') Accessible-name locator
cy.contains('button','Submit') page.getByRole('button',{name:'Submit'}) Role-based locator
.should('be.visible') await expect(loc).toBeVisible() Web-first assertion, auto-retries
.should('have.text','y') await expect(loc).toHaveText('y') Independent retrying assertion
cy.url().should('include','/x') await expect(page).toHaveURL(/\/x/) Page-level assertion
cy.intercept(...).as('r') await page.route('**/api', route => ...) Set route BEFORE the action
cy.wait('@r') await page.waitForResponse('**/api') Or just assert on resulting UI
cy.session() storageState (setup project) Save auth once, reuse
Cypress.Commands.add() test.extend() fixture DI + guaranteed teardown
cy.within(...) chained locator Scope by chaining, not nesting
Cypress.env('X') process.env.X Standard Node env
cy.screenshot('n') await page.screenshot({path:'n.png'}) Captures screenshots
cy.viewport(w,h) await page.setViewportSize({width,height}) Simulate screen sizes
cy.title() await page.title() Get page title

7. Handle waits and timing

Playwright has auto-waiting built in, which reduces flakiness:

auto-wait-example.spec.ts
await page.locator('button#submit').click();  // waits automatically

For explicit waits:

explicit-wait-example.spec.ts
await page.waitForSelector('#status', { state: 'visible' });

8. Convert intercept patterns and explore advanced use cases

Set up routes before the action that triggers the request. This is the single most common migration bug.

Network interception:

network-intercept.spec.ts
// Register the route BEFORE navigating
await page.route('**/api/products', route =>
  route.fulfill({ body: JSON.stringify([/* ...mock data... */]) }));
await page.goto('/products');

Abort requests:

abort-requests.spec.ts
await page.route('**/api/*', route => route.abort());

Multiple browser contexts:

multi-context.spec.ts
const context = await browser.newContext();
const page = await context.newPage();

This enables parallelism, multi-context testing, and robust strategies that Cypress alone cannot fully support.

9. Run, debug, then remove Cypress

Run tests in debug mode:

terminal
npx playwright test --debug

Use the trace viewer for step-by-step replay:

terminal
npx playwright show-trace trace.zip

Once every spec passes in CI on Playwright, update the CI pipeline to replace the Cypress step with npx playwright test (enable built-in parallelism and upload the HTML report + traces as artifacts), then delete Cypress, its config, and its dependencies.

Migrating a Cypress test to Playwright

It helps to see direct before/after translations. These reduce flakiness and make the migration smoother.

Example 1: Visiting a page and clicking a button

visit-click.cy.js
it('visit and click', () => {
  cy.visit('https://example.com');
  cy.get('button#start').click();
});

visit-click.spec.ts
test('visit and click'async ({ page }) => {
  await page.goto('https://example.com');
  await page.locator('button#start').click();
});

Example 2: Filling a form field

fill-form.cy.js
it('fill form', () => {
  cy.visit('/login');
  cy.get('#username').type('user');
  cy.get('#password').type('pass');
  cy.get('button[type=submit]').click();
});

fill-form.spec.ts
// Playwright (semantic locators + fill)
test('fill form'async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Username').fill('user');
  await page.getByLabel('Password').fill('pass');
  await page.getByRole('button', { name: 'Submit' }).click();
});

Example 3: Waiting for an element to appear

wait-element.cy.js
it('wait for element', () => {
  cy.get('#status', { timeout: 10000 }).should('be.visible');
});

wait-element.spec.ts
// Playwright (web-first assertion auto-waits)
test('wait for element'async ({ page }) => {
  await expect(page.locator('#status')).toBeVisible();
});

Is Playwright worth the switch?

Teams often start with Cypress because it's simple and intuitive, but as projects grow the limitations become clear. This is where Playwright shows its value, making the migration worthwhile for most modern QA workflows.

Benefits of Playwright vs Cypress limitations

1. Broader browser coverage: unlike Cypress, which is mostly Chrome-focused with limited Firefox and Edge support, Playwright runs on Chromium, WebKit, and Firefox. This enables true cross-browser testing across all major environments.

2. Native parallelism and isolation: Playwright runs multiple tests simultaneously in isolated browser contexts, cutting CI/CD pipeline time and preventing test interference. Cypress runs tests largely in a single tab, limiting scalability.

3. Fewer flaky tests due to auto-wait: Playwright's built-in auto-waiting handles asynchronous behavior automatically, so tests are less likely to fail on timing. Cypress often needs manual waits or retries, which makes large suites brittle.

4. Headless execution by default: Playwright is optimized for headless mode, making runs faster and easier to integrate into CI/CD. Cypress can run headless but sometimes needs extra configuration.

5. Integrated tracing and debugging: Playwright ships a trace viewer, network monitoring, and step-by-step replay, so teams debug flaky tests efficiently.

Switching to Playwright is more than a framework change; it's an investment in scalable, stable, future-proof automation.

When you might still keep Cypress around

1. For smaller single-browser UI tests: if your project targets Chromium-based browsers and the suite is small, Cypress's simplicity makes tests fast to write and maintain.

2. When the team is heavily invested in Cypress syntax: teams fluent in Cypress patterns may stay more productive with it for smaller projects or prototypes.

3. If no multi-browser or CI/CD scalability is required: for lightweight projects, the overhead of migrating might outweigh the benefits.

In short, Cypress remains valid for targeted, small-scale testing, but as soon as you need broader coverage, higher reliability, and CI/CD efficiency, a Cypress to Playwright migration becomes worthwhile.

10 mistakes teams make migrating Cypress to Playwright

Set the route before the action

Set the route before the action

  1. Forgetting locators are lazy. page.getByRole('button') queries nothing until you act on it, so don't expect a Cypress-style immediate DOM read.
  2. Chaining assertions. Each expect() is independent and auto-retries on its own; don't port .should().and() chains literally.
  3. Using type() everywhere. fill() sets value instantly; reserve pressSequentially() for keystroke-dependent UIs.
  4. Asserting visibility without scroll. Actions auto-scroll; assertions don't. Call scrollIntoViewIfNeeded() first when needed.
  5. Looking for cy.wrap(). In async/await, you just use the value directly.
  6. Setting page.route() too late. It must be registered before the action that triggers the request; after page.goto() is too late.
  7. Expecting a Cypress "subject." Use textContent(), inputValue(), or a web-first assertion instead of a chained subject.
  8. Keeping a plugin layer. Tests run in Node, so call DB/filesystem code directly from fixtures or globalSetup.
  9. Porting cy.within() as nested callbacks. Scope by chaining locators instead.
  10. Fighting test isolation. Every test gets a fresh context, so there's no manual cleanup, but you can't intentionally leak state between tests.

Leveraging TestDino for post-migration analytics

After your Playwright environment is live, keep improving visibility and control. That's where TestDino comes in, turning raw test data into actionable insight.

AI failure grouping automatically clusters similar failures, so you spot repeated issues instead of scrolling through long failure lists.

Migration health tracking gives a live view of flaky tests, pass-rate trends, and performance changes, so you instantly see whether your new Playwright setup is stabilizing or needs tuning.

Unified Playwright reports combine results, screenshots, and traces in one place, with smart alerts for critical failures so you're not drowning in noise.

With Playwright test reporting in TestDino, your reports become visual, traceable, and data-driven, cutting debugging time and keeping release confidence high.

Conclusion

Migrating from Cypress to Playwright isn't just a technical shift; it's a strategic move toward a scalable, future-ready testing ecosystem. Playwright gives QA teams multi-browser coverage, faster execution, and reduced flakiness, so your automation keeps pace with product demands.

Beyond speed, Playwright's architecture brings depth to debugging and performance tracking: built-in tracing, network analysis, and parallel execution mean less time maintaining flaky tests and more time improving real product quality.

Paired with TestDino, your Playwright migration becomes truly data-driven, with AI-powered analytics, failure grouping, and real-time trend monitoring giving you full visibility into every run.

FAQs

Is it difficult to migrate from Cypress to Playwright?
No. The migration is straightforward because Playwright's test structure closely mirrors Cypress. Most effort goes into mapping commands, handling async behavior, and updating selectors.

Can Cypress and Playwright coexist in the same project?
Yes. Many teams run Cypress and Playwright side by side during migration, allowing a gradual transition without disrupting existing CI pipelines or coverage.

Does Playwright reduce flaky tests compared to Cypress?
Yes. Playwright has built-in auto-waiting, isolated browser contexts, and retries, which significantly reduce the timing-related flakiness common in Cypress tests.

How long does it take to migrate from Cypress to Playwright?
For a small-to-medium suite, 1–3 days is typical when you migrate file-by-file and run both runners in parallel. Large suites (thousands of specs) have been migrated in a few months, often with AI-assisted conversion plus manual review of every file.

Is there a tool to convert Cypress code to Playwright?
Yes. Converters like the official cy2pw playground translate common Cypress commands to Playwright. They handle the mechanical mapping, but review every converted file because timing-dependent tests that passed in Cypress can behave differently under Playwright's auto-waiting.

What's the hardest part of the migration?
The mental-model shift, not the syntax. The two changes that trip teams up most are network mocking (page.route() must be set up before the triggering action) and replacing custom commands with fixtures. Once those click, the rest is command-by-command mapping.
Pratik Patel

Founder & CEO

Pratik Patel is the founder of TestDino, a Playwright-focused observability and CI optimization platform that gives engineering and QA teams clear visibility into test results, flaky failures, and pipeline health. With 12+ years in QA automation, he has helped startups and enterprises like Scotts Miracle-Gro, Avenue One, and Huma build and scale high-performing QA teams. An active open-source contributor, he regularly writes about modern testing practices, Playwright, and developer productivity.

Get started fast

Step-by-step guides, real-world examples, and proven strategies to maximize your test reporting success