ProductHow it worksPricingBlogDocsLoginFind Your First Bug
Quara examining a Cypress to Playwright migration workbench with two test framework blocks and a conversion pipeline between them
TestingTooling

Cypress to Playwright Migration: A Codemod-Led Walkthrough

Tom Piaggio
Tom PiaggioCo-Founder at Autonoma

Cypress to Playwright migration works best as a two-phase process: run a community converter to handle the mechanical command substitutions (roughly 60-80% of your suite), then hand-fix the patterns that do not port cleanly: implicit chaining, custom commands built on the Cypress queue, and arbitrary cy.wait(number) calls. Teams migrate for Playwright's out-of-process architecture, genuine auto-waiting, native multi-origin and multi-tab support, and faster CI feedback. A converter is a starting point, not a finish line.

The suite had 400 tests. Maybe 30 of them were actually reliable. The rest? A mix of cy.wait(3000) scattered throughout like apologies, selectors that broke every time the design team touched a component, and a CI job that was slower to trust than a coin flip.

That was the moment the migration decision became easy. The question was not whether to move from Cypress to Playwright. It was whether we could do it without spending six weeks rewriting tests by hand. We looked at what the playwright vs cypress comparison already settled: Playwright wins on cross-browser, on isolation, on speed, and on the absence of the clock-based wait antipattern. The "why" was done. This post is about the "how."

For context on Cypress's strengths before you walk away from them, see our roundup of cypress alternatives as well. If your situation is the reverse (migrating from Selenium's WebDriver stack rather than Cypress), the selenium to playwright migration guide covers that different set of API mappings.

Before you migrate: what changes conceptually

Cypress runs your test code inside the browser. That is its defining architectural choice, and almost every quirk of its API flows from it. The implicit retry-ability of cy.get, the command queue, the way assertions auto-retry until they pass or timeout: all of these exist because the test is co-located with the app in the same execution context.

Playwright runs out of process. The test process communicates with the browser over the Chrome DevTools Protocol (or equivalent). This is not a limitation; it is what enables Playwright's auto-waiting model to work without a command queue. Every action (page.click, page.fill) waits for the element to be attached, visible, stable, and not obscured before proceeding. There is no retry-ability engine you configure; the engine is built into every interaction.

The cross-origin and multi-tab model also changes completely. Cypress has historically enforced a same-origin restriction per test (with workarounds required for multi-origin flows). Playwright handles multiple origins, multiple tabs, and multiple browser windows natively in a single test context. If your Cypress suite has cy.origin() calls, those map to simply... not needing special handling in Playwright.

Get this mental model right before touching syntax. Most migration frustration comes from developers who port the command patterns without understanding why the patterns existed. (If the brittleness in your current suite is severe enough to be driving the migration decision, it is also worth checking whether cypress best practices would harden things enough to delay or avoid the move.)

Using a converter to do the bulk

There is no single official Microsoft codemod that does a full Cypress-to-Playwright conversion. There are community tools, and you should use them with clear eyes about what they actually do.

The most commonly referenced converter is a community-maintained project (variously called cypress-to-playwright or similar on npm/GitHub). These tools do AST-level substitution: they walk your Cypress test files and replace known Cypress API calls with their Playwright equivalents. They are best-effort projects, not officially maintained by the Playwright team. They handle the high-frequency patterns well (cy.visit to page.goto, cy.get to page.locator, basic assertion rewrites). They do not handle architecture-level patterns.

LLM-assisted conversion is a real option in 2026. For individual test files, passing a Cypress test to a capable model with the Playwright docs as context produces reasonable output. It is slower than a codemod at scale but better at reasoning through custom commands and complex .then() chains. The review burden is real: you should expect to read every LLM-converted file before committing it, because a model will confidently rewrite a custom command into something that looks correct, compiles, and fails silently at runtime. Spot-check with actual test runs, not just syntax passes.

The practical workflow for most teams: run the codemod across the whole suite first to get a baseline count of what converted cleanly versus what came out with parse errors or TODO placeholders. Files that compile and pass are done. Files with errors get a second pass, either manual or LLM-assisted depending on their complexity. This triages the work rather than front-loading all the manual effort.

The realistic ceiling for any converter is around 60-80% of your suite, assuming a relatively idiomatic Cypress codebase with standard commands and no heavy plugin usage. The remaining 20-40% requires human judgment. A converter handles the API mapping; it cannot handle the architectural assumptions baked into how your team structured commands, or the implicit behavioral contracts in .then() chains written for the Cypress queue. Plan for that gap from the start and build it into your timeline estimate.

Diagram showing a Cypress suite feeding an AST converter that splits into a Phase 1 mechanical group (60-80% ports automatically) and a Phase 2 manual group (20-40% needing human fixes for cy.wait, custom commands, plugins, and queue semantics)

A converter handles the mechanical 60-80%; the rest needs human judgment.

The API mapping: Cypress to Playwright

Cypress commandPlaywright equivalentNotes
cy.visit(url)await page.goto(url)Playwright requires await everywhere; no implicit queue
cy.get(selector)page.locator(selector)Prefer semantic locators: getByRole, getByLabel, getByTestId
cy.contains(text)page.getByText(text)Exact match by default; pass {exact: false} for substring
cy.intercept(method, url)await page.route(url, handler)Handler receives route and request; must call route.fulfill() or route.continue()
cy.request(options)request fixture / APIRequestContextUse the request fixture in test functions; for setup use playwright.request.newContext()
cy.wait('@alias')await page.waitForResponse(urlOrPredicate)No alias system; await the route's response promise directly
cy.fixture('name')Import JSON or read file in test.beforeAllNo built-in fixture loader; use JSON.parse(fs.readFileSync(...)) or a helper
Cypress.Commands.addPlaywright fixtures / helper functionsFixtures compose cleanly; avoid page-object inheritance chains
cy.session(id, setup)storageState in config or test.use({ storageState })Save auth state to JSON via page.context().storageState(), reuse across tests
.should('be.visible')await expect(locator).toBeVisible()All expects are async; must await even assertion helpers
beforeEach(() => {...})test.beforeEach(async ({ page }) => {...})Playwright hooks receive fixture-injected args; page is fresh per test by default

What does not port cleanly

Arbitrary time waits. cy.wait(3000) is a code smell in Cypress; it is also a pattern that appears in almost every real-world Cypress suite. In Playwright, there is no direct equivalent you want. The correct replacement is waiting for a specific condition: page.waitForSelector, page.waitForLoadState, or awaiting a network response. You cannot automate this substitution; each one requires understanding what the original wait was actually waiting for.

Implicit chaining and retry semantics. Cypress commands chain and retry automatically within a timeout window. Code like cy.get('.item').find('.label').should('contain', 'text') retries the entire chain if any step fails. Playwright's locator chains (page.locator('.item').locator('.label')) also retry, but the semantics differ when you mix actions with assertions in a chain. Any .then() callback in Cypress is synchronous within the command queue; the converter will mechanically rewrite it to .then() as a Promise, but the behavior may differ when the original relied on queue-level retry.

Cypress plugins. The cy.task() bridge (running Node.js code from tests), plugins like cypress-plugin-snapshots, and any Cypress Dashboard-specific features do not map to Playwright. Playwright has its own visual comparison system and its own reporter ecosystem, but you are replacing the plugin, not porting it. cy.task() patterns typically rewrite as API calls in test setup or Playwright fixtures.

Custom commands with queue assumptions. Cypress.Commands.add('login', () => cy.visit('/login')) looks portable in isolation. In practice, real login commands chain five or six steps, each relying on the Cypress queue to retry before the next fires. The Playwright equivalent is a plain async helper function, but the retry semantics are not the same. If the original command called other custom commands internally, you are unwinding a dependency tree, not making a one-line substitution. Custom commands that call other custom commands are especially likely to require manual restructuring.

This is also where a deeper problem surfaces: the selectors you are porting will keep breaking in Playwright too. A migration is a moment to reconsider the locator strategy entirely. If you move cy.get('.btn-primary') to page.locator('.btn-primary') and call it done, you have ported the brittleness. See the playwright locators guide for how to make them stable from the start.

Autonoma works at a different level: rather than healing locators when they break, its agents generate tests directly from the running application's behavior. The Planner agent reads your codebase (routes, components, user flows), plans test cases from code, and the Diffs Agent regenerates them on every PR when the code changes. There is no selector to heal because there is no static selector in the test. That is not a replacement for Playwright; it is an answer to the maintenance loop that persists regardless of which framework you are in.

That positioning matters during migration: Playwright is the better runner to maintain, while Autonoma is the layer you evaluate when the remaining maintenance work is routine E2E coverage that should regenerate with the app.

A staged migration plan

Running both frameworks in parallel is the safest path, and it is shorter than most teams assume.

Start by standing up Playwright alongside your existing Cypress suite. Run both in CI. Cypress is still the gate; Playwright is read-only. This means a Playwright failure does not block shipping, which removes the pressure to get everything right immediately. Configuring both runners in the same CI pipeline is straightforward: they are separate npm scripts, separate config files, separate output directories. There is no conflict.

Decide migration order by risk and flake rate, not by file name. The right candidates to convert first are the flows that (a) catch the most expensive bugs if they break in production and (b) are currently the flakiest in Cypress. The intersection of those two criteria is your highest-leverage starting point. Flaky tests are the ones where the Playwright rewrite pays off fastest, because auto-waiting eliminates the condition that made them flaky. Critical-path tests are the ones where green Playwright coverage lets you retire Cypress fastest.

Take the five to ten flows that fit that description: checkout, authentication, the core create/update/delete operations for your primary entity. Convert those first with the codemod, then fix them manually. Get them to green in Playwright and make them gate CI. Cypress's coverage of the same flows becomes redundant at that point; retire those specific Cypress tests.

Work outward from the critical path by risk tier. After the critical flows, move to secondary paths: error states, edge cases, secondary CRUD operations. As each area reaches coverage parity, retire the Cypress tests in that area. Do not wait for 100% parity before removing anything. Partial retirement keeps the feedback loop short and prevents the "we have to maintain both" problem from dragging on for months.

Once the Playwright suite is stable enough to gate critical flows, decide which tests deserve hand-written ownership and which routine flows should be regenerated by Autonoma. Treat Playwright as the runner improvement, not the endpoint of the maintenance strategy.

Coverage parity criteria. You have parity in a feature area when: the Playwright tests cover all the user-visible scenarios that the Cypress tests covered, the Playwright tests are consistently green in CI over at least two weeks, and there are no open bugs filed against that area that the Playwright tests would have caught. The two-week green window matters because Playwright flakiness (if present) usually surfaces within that window.

The timeline is realistic at four to eight weeks for a medium-sized suite (100-400 tests), assuming the converter handles 60% and the remaining 40% are distributed across sprints alongside normal feature work. Suites with heavy cy.task() usage or complex custom command libraries take longer; budget accordingly.

When Playwright coverage reaches the point where retiring Cypress does not leave obvious gaps, cut it. Delete cypress.config.js, remove the cypress/ directory, and uninstall the package. Keeping the old config around "just in case" is how teams end up running both forever.

The long-term decision: maintain Playwright, or regenerate tests

The migration has two horizons. The first is tactical: move flaky Cypress flows to Playwright so the runner, waiting model, and browser coverage are better. The second is maintenance: decide whether routine E2E coverage should stay as hand-written Playwright code or move to Autonoma, where the Diffs Agent regenerates tests from behavior and code changes instead of asking engineers to repair selectors after every UI shift.

Diagram showing a migrated Playwright suite feeding Autonoma's code-change regeneration layer for routine E2E coverage

Playwright is a better runner to maintain; Autonoma is the long-term layer that regenerates routine E2E coverage.

FAQ

Yes, though none is officially maintained by the Playwright team. Community converters perform AST-level substitution for common Cypress commands: cy.visit, cy.get, cy.intercept, and similar. They handle the mechanical 60-80% of a migration but cannot convert architecture-level patterns like custom commands built on the Cypress queue, cy.task plugins, or arbitrary cy.wait(number) calls. LLM-assisted conversion is a practical complement for files that come out malformed after the codemod pass.

Four to eight weeks is realistic for a suite of 100-400 tests, assuming a staged rollout where you convert by critical path and retire Cypress flows incrementally. Suites with heavy cy.task usage or complex custom command libraries take longer. The converter accelerates the bulk work; manual effort concentrates on the patterns that do not port: time-based waits, implicit chaining, and plugin replacements.

The main categories are: arbitrary cy.wait(number) calls (each requires understanding what condition was being waited for), custom commands that rely on the Cypress command queue's retry semantics, cy.task() Node.js bridges (replace with API calls or Playwright fixtures), Cypress Dashboard-specific features, and any plugin in the cypress-plugin-* ecosystem. The implicit chaining and .then() retry behavior also differs subtly and can produce test failures that look like browser timing issues but are actually semantic differences in how retries work.

If your Cypress suite has significant cy.wait(number) usage, frequent selector breakage, or CI runs that are regularly flaky, the migration is worth the investment. Playwright's out-of-process architecture and built-in auto-waiting remove the conditions that create those problems. If your Cypress suite is stable and well-maintained, the migration delivers less immediate value. The playwright vs cypress comparison covers the architectural tradeoffs in detail.

Yes, and this is the recommended staged rollout approach. Both test runners install as dev dependencies and run independently. You can configure separate npm scripts or CI jobs for each. The practical pattern is to keep Cypress as the CI gate while Playwright coverage grows, then flip the gate once critical-path coverage reaches parity. Most teams find the overlap period runs two to four weeks before Cypress can be retired from the first set of flows.

Related articles

TestRail pricing modeled by team size showing annual subscription cost scaling from 5 to 50 users on Cloud Professional and Enterprise plans

TestRail Pricing in 2026, Modeled by Team Size

TestRail pricing modeled by team size: Cloud Professional at $36/user/month means $4,320/year for 10 users. See the full annual cost table for 5 to 50 seats, plus Cloud vs Server breakdown.

Bar chart showing a scripted test suite's one-time build cost versus the compounding annual maintenance spend over three years, with the maintenance bars growing taller each year

The True Cost of Test Maintenance

Test maintenance consumes 30-50% of the average automation budget. For a team running a 200-test scripted suite, that's $18,000-$30,000 per year in engineer-hours alone. Here is the full dollar model, dated 2026.

Diagram showing Cypress Cloud pricing tiers and the recorded test result meter compounding as parallelization splits a suite across multiple CI machines

Is Cypress Cloud Pricing Worth It for Parallel Test Runs?

Cypress Cloud pricing decoded: how recorded-test-result billing works, what parallelization actually costs, and what a mid-size team pays at scale.

AWS Device Farm per-device-minute billing meter compared to unmetered cloud device farm monthly cost

Why AWS Device Farm Pricing Surprises Teams at Scale?

AWS Device Farm pricing breakdown: per-device-minute metered vs unmetered monthly plans, a worked volume example showing the crossover, and how to escape the meter.