Anastasiia Sokolinska

Written by: Chief Operating Officer

Anastasiia Sokolinska

Posted: 02.06.2026

19 min read

All tools have limits.

In the case of Cypress, that limit appears to occur around 300 tests. What was previously an 8 minute CI pipeline has now become a 40 minute CI pipeline. Tests that pass when run locally do not pass when run as part of CI, but there is no way to determine why this is happening, so you continue to add your own fixes, which ultimately fail, and end up being forced to keep adding your own fixes.

Playwright was built to be used on the modern Web that we use today with capabilities such as multiple window tabs, cross-origin flows, etc. Without being required to pay (paywall), without the use of any plugins, and without being required to use the queuing commands that are now built into the framework that are neither asynchronous nor synchronous. Fast, reliable and true tests.

As of 2024, the engineering community has voted using download numbers and has placed Playwright above Cypress. The gap continues to get wider.

This is a guide to show you the best way to make the switch!

Need help with the migration?

Contact us

Why teams are migrating from cypress to Playwright

To understand why Playwright has become the preferred framework for new projects and migrations alike, you need to look at the architectural decisions Cypress made in its early years, and how those decisions create friction at scale.

Cypress runs inside the browser, which gave it early advantages: fast feedback, intuitive debugging, and easy setup. But that same in-browser execution model also imposes hard limits:

  • Single-tab execution only. Tests that span multiple tabs or windows require workarounds that break test isolation.

  • No true cross-browser support. Cypress supports Chromium-based browsers and has limited Firefox support. WebKit (Safari) is not supported — a genuine gap for teams building consumer applications.

  • Parallel execution is a paid feature. Running tests in parallel requires Cypress Cloud. Teams with large suites absorb that cost or accept slow CI times.

  • Command chaining instead of async/await. Cypress uses a custom command queue that looks synchronous but isn't. This trips up new team members and makes certain test patterns awkward to express.

  • Flakiness in CI environments. Timing-dependent tests that pass locally often fail in headless CI runs because Cypress lacks fine-grained control over element visibility and network state.

Playwright was built to solve these problems directly. Developed by the same Microsoft team that created Puppeteer, it uses a browser driver model that operates outside the browser process, giving it cross-browser control, multi-context isolation, and native async/await throughout.

Feature
Cypress
Playwright

Browser support

Chromium, partial Firefox

Chromium, Firefox, WebKit

Parallel execution

Paid (Cypress Cloud)

Built-in, free

Test isolation

Shared state, single tab

Independent browser contexts

Async model

Command queue (custom)

Native async/await

Network interception

cy.intercept() — basic

page.route() — advanced, flexible

Language support

JavaScript only

JS, TS, Python, C#, Java

Debugging tools

Time Travel, DevTools GUI

Trace Viewer, Inspector, CLI

Flakiness handling

Manual waits required

Auto-wait built-in

CI/CD integration

Works; requires config

Native, optimised for pipelines

Mobile emulation

Limited

Full device emulation (iOS/Android)

Via workarounds

Native alongside UI tests

Cross-origin testing

Blocked by architecture

Fully supported

Multi-tab support

Workarounds only

First-class feature

Should you migrate? A Decision framework

The honest answer is: it depends on your suite, your team, and your product roadmap. Here's how to think through it.

Signs your team is ready to migrate

  • Your CI test runs are taking 20+ minutes on a suite that should be faster

  • You've hit the limit of what Cypress Cloud parallelization can do cost-effectively

  • You need cross-browser coverage, including Safari/WebKit

  • Tests are flaky in CI at a rate that's causing real productivity loss

  • Your application includes multi-tab flows, cross-origin interactions, or complex auth scenarios

  • You're building a new testing layer from scratch (greenfield Playwright > greenfield Cypress today)

  • Your team already writes TypeScript and wants native type safety in tests

Reasons to hold off

  • Your Cypress suite is small (<100 tests) and stable — migration effort won't pay off quickly

  • Your team has deep Cypress expertise and no cross-browser or parallelism pain

  • You have no engineering capacity allocated to the migration — a rushed migration creates more flakiness, not less

  • You're in a feature-freeze or delivery crunch — migration is an investment, not a quick fix

Readiness tier
Profile
Recommendation

Ready

100+ tests, CI pain, cross-browser need, TS team

Begin incremental migration now

Assess first

50—100 tests, occasional flakiness, Cypress-only pain

Pilot Playwright on 1 feature area first

Hold

<50 tests, stable CI, no cross-browser need

Revisit in 6—12 months or on next major refactor

Skip the trial and error — our engineers have done this before.

Contact us

Before you migrate: Planning and preparation

The teams that struggle with migration are almost always the ones that start writing Playwright code before they've finished auditing their Cypress suite. Preparation isn't bureaucracy — it's what keeps migration from becoming a months-long headache.

Step 1: Audit your existing Cypress suite

Walk through your test suite and catalogue:

  • Custom commands — anything in cypress/support/commands.js. These will all need to be refactored.

  • Third-party plugins — cypress-axe, cypress-real-events, cypress-file-upload, etc. Check Playwright equivalents before starting.

  • Fixtures and test data — the fixtures/ folder structure will need to be replicated.

  • Shared utilities — auth setup, API helpers, environment config.

Step 2: Categorise tests by migration complexity

Complexity tier
Characteristics
Effort estimate

Straightforward

Basic UI flows, no custom commands, simple assertions

1—2 hours per spec file

Moderate

POM classes, shared auth, fixtures, basic network mocking

3—5 hours per spec file

Complex

Custom commands, iframes, shadow DOM, multi-tab, cross-origin

1—2 days per spec file

Step 3: Choose a migration strategy

Incremental migration is the right approach for almost all teams. Migrating everything at once ('big bang') means you lose CI coverage during the transition and create a long period of test debt. With an incremental approach, you run Playwright for new and migrated tests, keep Cypress for legacy tests, and deprecate Cypress coverage progressively.

A realistic timeline for a team with 200—400 Cypress tests: 3 months total, with the first month being the highest intensity (infrastructure setup, core utilities, auth flows, CI integration).

Setting up Playwright in your project

Playwright's CLI scaffolds everything you need to get started. Run this in the root of your project:

npm init playwright@latest

The CLI will ask which browsers to install, whether to use TypeScript (yes — strongly recommended), where to put your test files, and whether to add a GitHub Actions workflow. After setup, your project gains:

playwright.config.ts   # Main configuration
tests/                 # Your spec files go here
tests-examples/        # Sample tests (can be deleted)
.github/workflows/     # CI workflow (if selected)

playwright.config.ts — key settings to configure

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true, // Run tests in parallel by default
  retries: process.env.CI ? 2 : 0, // Retry on CI only
  workers: process.env.CI ? 4 : undefined,
  reporter: 'html', // Built-in HTML report
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry', // Capture trace on failure
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

Migrating your tests: Command and syntax mapping

This is where the actual migration work happens. The good news: Playwright's test structure closely mirrors Cypress's, so the mental model transfers. The differences are mostly in syntax, async handling, and how you interact with the DOM.

Navigation and basic actions

Cypress
Playwright
Notes

cy.visit('/login')

await page.goto('/login')

Playwright uses baseURL from config

cy.get('#btn').click()

await page.locator('#btn').click()

Or use role/text selectors

cy.get('input').type('text')

await page.locator('input')
.fill('text')

fill() clears then types

cy.get('input').clear()

await page.locator('input')
.clear()

Direct equivalent

cy.get('a').contains('Sign in')

await page.getByText('Sign in')

Playwright prefers semantic selectors

cy.get('[data-cy=btn]')

await page.getByTestId('btn')

Set testIdAttribute in config if needed

cy.reload()

await page.reload()

Direct equivalent

cy.go('back')

await page.goBack()

Direct equivalent

cy.wait(1000)

await page.waitForTimeout(1000)

Avoid in both — use auto-wait instead

Assertions

Cypress
Playwright
Notes

cy.get('#title')
.should('be.visible')

await expect(locator)
.toBeVisible()

Auto-waits for element

cy.get('#title')
.should('have.text', 'Hi')

await expect(locator)
.toHaveText('Hi')

Supports regex

cy.url().should('include', '/dashboard')

await expect(page)
.toHaveURL(/dashboard/)

Page-level assertion

cy.get('input')
.should('have.value', 'abc')

await expect(locator)
.toHaveValue('abc')

cy.get('li')
.should('have.length', 5)

await expect(locator)
.toHaveCount(5)

cy.get('#msg')
.should('not.exist')

await expect(locator).not
.toBeVisible()

Or toHaveCount(0)

cy.title().should('eq', 'Home')

await expect(page)
.toHaveTitle('Home')

Async/await — the most important conceptual shift

One of the most confusing things about migrating to Cypress has to do with the way that Cypress utilizes a command queue to execute commands. When commands are executed in Cypress, they appear to be executed synchronously; however, in actuality they are executed asynchronously.

Playwright uses the native JavaScript async/await syntax throughout the Playwright library and allows your Playwright tests to look exactly like the code you write in other parts of your application that use async/await.

Here's the same login test in both frameworks:

// CYPRESS
it('should log in successfully', () => {
  cy.visit('/login');
  cy.get('#username').type('admin@example.com');
  cy.get('#password').type('password123');
  cy.get('[data-cy=login-btn]').click();
  cy.url().should('include', '/dashboard');
  cy.get('h1').should('contain', 'Welcome');
});
// PLAYWRIGHT
test('should log in successfully', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('admin@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByTestId('login-btn').click();
  await expect(page).toHaveURL(/dashboard/);
  await expect(page.getByRole('heading', { level: 1 })).toContainText('Welcome');
});

Note the shift toward semantic selectors (getByLabel, getByRole, getByTestId) rather than CSS selectors. Playwright's recommendation is to prefer selectors that reflect what a user sees — this makes tests more resilient to DOM changes.

Network interception and mocking

// CYPRESS
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
cy.wait('@getUsers');

// PLAYWRIGHT
await page.route('**/api/users', async route => {
  const json = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
  await route.fulfill({ json });
});
// No need to explicitly wait — Playwright auto-waits for route to be triggered

Your team builds the product. We'll build the test infrastructure.

Contact us

Migrating Page Object Model and custom commands

This is the section most migration guides skip — and it's the one that takes the most time. If your Cypress suite has a Page Object Model or shared custom commands, those need to be rebuilt, not just translated line by line.

Migrating Cypress custom commands to Playwright fixtures

Cypress custom commands are defined globally. Playwright uses fixtures — a dependency injection pattern that's more explicit and testable.

// CYPRESS — cypress/support/commands.ts
Cypress.Commands.add('login', (email: string, password: string) => {
  cy.visit('/login');
  cy.get('#email').type(email);
  cy.get('#password').type(password);
  cy.get('[data-cy=submit]').click();
  cy.url().should('include', '/dashboard');
});

// Usage in tests:
cy.login('admin@example.com', 'password123');
// PLAYWRIGHT — tests/fixtures/auth.fixture.ts
import { test as base, Page } from '@playwright/test';

type AuthFixtures = {
  loginAs: (email: string, password: string) => Promise<void>;
};

export const test = base.extend<AuthFixtures>({
  loginAs: async ({ page }, use) => {
    await use(async (email: string, password: string) => {
      await page.goto('/login');
      await page.getByLabel('Email').fill(email);
      await page.getByLabel('Password').fill(password);
      await page.getByRole('button', { name: 'Sign in' }).click();
      await expect(page).toHaveURL(/dashboard/);
    });
  },
});

// Usage in tests:
import { test } from '../fixtures/auth.fixture';

test('admin can view reports', async ({ page, loginAs }) => {
  await loginAs('admin@example.com', 'password123');
  // ... rest of test
});

Migrating a Page Object class

Cypress Page Objects often call cy.* directly inside their methods. Playwright POMs receive the page object in their constructor and use it explicitly — a cleaner, more testable pattern.

// CYPRESS — cypress/pages/LoginPage.ts
export class LoginPage {
  visit() {
    cy.visit('/login');
  }
  fillEmail(email: string) {
    cy.get('#email').type(email);
  }
  fillPassword(password: string) {
    cy.get('#password').type(password);
  }
  submit() {
    cy.get('[data-cy=submit]').click();
  }
}
// PLAYWRIGHT — pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign in' });
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
    await expect(this.page).toHaveURL(/dashboard/);
  }
}

// Usage in tests:
test('user can log in', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@example.com', 'pass');
});

Hint: This is your chance to take care of the technical debt that tends to be accrued in Cypress Page Object Model tests, methods using cy.wait() calls, overly complex command chains, and superfluous utility functions. The asynchronous nature of Playwright makes it necessary to do so.

Running Cypress and Playwright side by side

Most teams cannot freeze delivery to migrate their entire test suite. The practical answer is to run both frameworks simultaneously in CI for the duration of the migration — Playwright picks up new tests and migrated coverage, Cypress handles everything not yet migrated.

Repository structure

project-root/
├── cypress/          # Existing Cypress suite (legacy)
│   ├── e2e/
│   ├── support/
│   └── cypress.config.ts
├── tests/            # New Playwright suite
│   ├── auth/
│   ├── checkout/
│   ├── fixtures/
│   └── pages/
└── playwright.config.ts

CI strategy during migration

  • Run both test runners in parallel CI jobs — don't block Playwright on Cypress completion.

  • Maintain a migration tracker — a simple spreadsheet or GitHub project board listing each spec file and its migration status.

  • Mark Cypress tests as 'pending deprecation' as soon as equivalent Playwright coverage is confirmed.

  • Set a sunset criterion — a clear rule for when to remove Cypress jobs. A sensible threshold: >90% of critical user journeys covered in Playwright, zero Cypress-only functionality remaining.

A common mistake is letting both suites run indefinitely without a deprecation plan. Set a hard deadline at the start of the migration — 'Cypress jobs are retired by [date]' — and enforce it. Indefinite coexistence becomes permanent coexistence.

Bring us your Cypress suite. We'll bring the Playwright expertise.

Contact us

Using AI tools to accelerate migration

For suites with hundreds of spec files, manual migration is feasible but slow. AI tools can meaningfully accelerate the mechanical parts of migration — command translation, syntax conversion, and basic POM structure — though they require careful review.

cy2pw — official Playwright converter

The official converter at demo.playwright.dev/cy2pw accepts a Cypress spec file and outputs a Playwright equivalent. It handles most common command mappings accurately. Best used for straightforward spec files with no custom commands. Always review output — it doesn't know your project's POM conventions or fixture setup.

GitHub Copilot / ChatGPT for spec file migration

For single spec files, a well-structured prompt gives good results. Here's a prompt template that works reliably:

You are a test automation engineer. Convert this Cypress test file to Playwright.

Rules:
- Use TypeScript with async/await throughout
- Replace cy.get() with page.getByRole(), getByLabel(), or getByTestId() where possible
- Replace cy.should() with expect() assertions
- Replace cy.intercept() with page.route()
- Do NOT use cy.wait() with time values — use Playwright auto-wait or waitForResponse
- Preserve the test structure and descriptions exactly
- Import test and expect from @playwright/test

Cypress file to convert:
[paste file contents here]

OpenAI API for batch migration

TenantCloud Engineering uses programmatically using OpenAI API for migrating a large suite (4,000 Files/3 Months) by iterating through spec files, converting each file using conversion prompts, then writing them to Playwright folders. The OpenAI API works best after you have implemented Playwright conventions (such as POM structure, fixture setup, import paths) so that the AI has examples to reference.

What AI handles well — and where it falls short

AI handles well
Requires human review

Basic command mapping (visit, click, type, get)

Custom Cypress commands — AI doesn't know what they do

Assertion translation

POM structure and fixture wiring

Async/await conversion

Business logic embedded in test setup

Simple network intercept conversion

Shared authentication and storage state

Renaming and import updates

Test data and fixture file references

CI/CD integration with Playwright

GitHub Actions

# .github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
jobs:
  playwright:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright browsers
        run: npx playwright install --with-deps
      - name: Run Playwright tests
        run: npx playwright test
      - name: Upload test report
        uses: actions/upload-artifact@v4
        if: always() # Upload even on failure
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14

Parallel execution with sharding

Playwright's built-in sharding splits your test suite across multiple CI machines without any third-party service:

# Split into 4 shards across 4 CI jobs
npx playwright test --shard=1/4  # Machine 1
npx playwright test --shard=2/4  # Machine 2
npx playwright test --shard=3/4  # Machine 3
npx playwright test --shard=4/4  # Machine 4

This alone can eliminate the need for a Cypress Cloud subscription for teams whose primary use of that service was parallelization.

Debugging in Playwright

Cypress's Time Travel Debugger — the ability to click back through test steps in a visual UI — is one of its most praised features. Playwright doesn't replicate that exact experience, but Trace Viewer provides equivalent (and often more detailed) information.

Trace Viewer

The Trace Viewer is Playwright's most powerful debugging tool. It records a complete timeline of each test run — DOM snapshots, network requests, console logs, and screenshots — and lets you step through the execution after the fact.

Enable tracing in playwright.config.ts:

use: {
  trace: 'on-first-retry', // Captures trace when a test is retried
  // or 'on'              // Always capture (larger files, slower runs)
  // or 'retain-on-failure' // Capture only for failing tests
}

To open the trace after a run:

npx playwright show-trace test-results/path-to-trace.zip

Playwright Inspector — step-through debugging

# Run a specific test in debug mode
npx playwright test login.spec.ts --debug

This opens the Inspector UI, which lets you step through test actions one by one, inspect locators in real time, and pause execution at any point. Equivalent to breakpoint debugging, directly in the browser.

Screenshots and video on failure

When configured with screenshot: 'only-on-failure' and video: 'retain-on-failure' in playwright.config.ts, Playwright automatically captures both on any failing test. These are attached to the HTML report and to CI artifacts. No plugin required — it's part of the framework.

Let our engineers do the migration while yours ship the product.

Contact us

Measuring migration success

This is where most migration guides end without actually finishing the job. Running Playwright tests isn't success — a measurably faster, more stable, and more maintainable test suite is success. Here's how to measure it.

Metric
How to measure
Target benchmark

CI execution time

Compare average run time before and after for equivalent test coverage

40—70% reduction

Flakiness rate

Track pass/fail ratio over 2-week windows pre- and post-migration

<2% flakiness rate

Test coverage

Ensure all critical user journeys present in Cypress are covered in Playwright

100% critical path parity

Maintenance time

Sprint retro: hours per sprint spent debugging/fixing broken tests

Meaningful reduction

Developer satisfaction

Short survey or retro question: 'How painful is writing and maintaining tests?'

Qualitative improvement

And those metrics do carry significance — teams have reported increases in test execution speeds by as much as 3.87 times and CI times by 60—70%. However, the metric that tends to come as a surprise to many teams is the maintenance one. The auto-waiting functionality and visibility checks provided by Playwright solve one entire class of bugs.

Recommendation: Set up your metrics before you begin migrating — don't wait until afterwards. In order to show the effects, you will need metrics beforehand. Grab your latest 30 days' worth of CI build times and flakiness rates from your CI tool.

Frequently asked questions

Is it difficult to migrate from Cypress to Playwright?

The transition process is easier than expected since the test structure is quite similar in both frameworks, meaning your mental model applies here as well. The most complicated part would be understanding how to work with async/await code in JavaScript, the locator API in Playwright, and refactoring custom commands to create fixtures.

How long does migration take?

If your test suite is medium-sized (200-400 tests), you will need two to three months to conduct the transition fully. If the test suite is small (<100 tests), two to four weeks will suffice. The first month will take the most effort — most of the time is spent on the test utilities and the CI configuration rather than on spec files refactoring.

Can Cypress and Playwright coexist in the same project?

Yes, it is. The combination of the two tools at the migration stage is encouraged. It is convenient to run them concurrently, since there are dedicated test files, configurations, and even CI jobs for each one.

Does Playwright actually reduce flaky tests?

Most probably. The automated waiting functionality, isolated browser environments, and retry logic provided by Playwright solve the issue of flakiness most of the time. However, some tests will require particular assertions.

Do I need to pay for Playwright's parallel execution?

Absolutely not. Playwright's parallelization and sharding capabilities have been seamlessly incorporated into the package without requiring any additional fees. One of the most obvious ways Playwright reduces costs relative to Cypress is that parallel testing requires buying a Cypress Cloud license.

Conclusion

Here's what the data shows.

Teams that migrate thoughtfully — not in a rush, not all at once — come out the other side with 60—70% faster CI runs, near-zero flakiness, and test suites they're no longer afraid to touch.

That's not a Playwright achievement. That's an engineering achievement.

Playwright just removes the friction that was in the way.

Not sure where to begin? Our automation engineers at DeviQA have helped many clients migrate from Cypress to Playwright on more than 40 different domains — from audit to full CI. If you'd rather use your engineering skills to develop a product than rebuild your testing framework, we are here to help.

You don't have to figure this out alone — we already did.

Contact us
Anastasiia Sokolinska

About the author

Anastasiia Sokolinska

Chief Operating Officer

Anastasiia Sokolinska is the Chief Operating Officer at DeviQA, responsible for operational strategy, delivery performance, and scaling QA services for complex software products.