← Back to DevBytes

Migrating from Cypress to Playwright: Step-by-Step Guide

Understanding the Migration from Cypress to Playwright

Migrating from Cypress to Playwright means transitioning your end-to-end testing suite from the Cypress test runner and its unique APIs to Microsoft’s Playwright framework. It’s not just a syntax swap – it’s adopting a different architecture, execution model, and set of superpowers that address many of Cypress’s long-standing limitations.

Why This Migration Matters

Cypress revolutionized browser testing with its developer-friendly UI, time-travel debugging, and automatic waits. However, as applications grow more complex and teams demand faster CI pipelines, Cypress reveals critical constraints:

Playwright addresses all of the above: it supports Chromium, Firefox, and WebKit (Safari) across all platforms; it offers full multi-page, multi-tab, and cross-origin scenarios; it includes mobile device emulation, geolocation, permissions, and a powerful API testing context. Its test runner runs fully in parallel by default, and it provides built-in auto-waiting, network interception, and modern debugging tools like trace viewer. Migrating to Playwright future-proofs your test suite and often reduces flakiness while improving execution speed.

Preparing for the Migration

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Before writing a single line of Playwright code, audit your existing Cypress test suite. Identify:

Create a migration plan: decide whether to migrate incrementally (both frameworks coexist temporarily) or do a full replacement. An incremental approach is safer – you can run both suites in CI until confidence is high.

Step-by-Step Migration Guide

1. Install Playwright and Initialize the Project

First, add Playwright to your project. It’s best to use the official installation wizard that sets up the test directory, configuration, and example tests:

npm init playwright@latest

This command asks you to choose between JavaScript/TypeScript, the test directory name (tests or e2e), and whether to add a GitHub Actions workflow. If you already have a test directory from Cypress, you can point Playwright to the same folder or create a new one (recommended: playwright-tests to keep them separate during incremental migration).

Alternatively, install manually:

npm install --save-dev @playwright/test
npx playwright install

2. Map Cypress Configuration to Playwright

Cypress cypress.config.js settings translate to Playwright’s playwright.config.ts (or .js). Here’s a typical conversion:

// Cypress cypress.config.js
module.exports = {
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    defaultCommandTimeout: 10000,
    retries: 2,
  },
}
// Playwright playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    baseURL: 'http://localhost:3000',
    viewport: { width: 1280, height: 720 },
    actionTimeout: 10000,
    // Playwright auto-waiting often makes explicit timeouts less necessary
  },
  retries: 2,
  // Parallelism: workers default to half of CPU cores, Cypress parallel requires Dashboard
  workers: process.env.CI ? 4 : undefined,
});

3. Convert Basic Test Structure

Cypress uses describe/it blocks from Mocha. Playwright’s test runner uses its own test block (based on Jest’s test/it), with describe still supported. The key difference: Playwright tests are asynchronous and rely on fixtures (like page) passed as arguments.

Cypress test:

describe('Login Flow', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('should display error for invalid credentials', () => {
    cy.get('#email').type('wrong@example.com');
    cy.get('#password').type('wrongpassword');
    cy.get('button[type=submit]').click();
    cy.get('.error-message').should('be.visible');
  });
});

Playwright equivalent:

import { test, expect } from '@playwright/test';

test.describe('Login Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('should display error for invalid credentials', async ({ page }) => {
    await page.locator('#email').fill('wrong@example.com');
    await page.locator('#password').fill('wrongpassword');
    await page.locator('button[type=submit]').click();
    await expect(page.locator('.error-message')).toBeVisible();
  });
});

Notice the replacement pattern:

4. Replace Common Cypress Commands

Assertions

Cypress chains assertions with .should(). Playwright uses the expect library (from Jest) with async matchers.

// Cypress
cy.get('.title').should('have.text', 'Welcome');
cy.get('input').should('have.value', 'John');
cy.get('.list').should('have.length', 3);

// Playwright
await expect(page.locator('.title')).toHaveText('Welcome');
await expect(page.locator('input')).toHaveValue('John');
await expect(page.locator('.list')).toHaveCount(3);

Network Interception & Fixtures

Cypress uses cy.intercept() and cy.fixture(). Playwright provides page.route() for intercepting requests and can fulfill with fixture data directly.

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

// Playwright – route interception
await page.route('**/api/users', async (route) => {
  const users = require('./fixtures/users.json'); // or import JSON
  await route.fulfill({ body: JSON.stringify(users) });
});

For static fixture files, Playwright can also use test.use({ storageState }) or simply import the JSON and fulfill the route.

Custom Commands (Cypress.Commands.add)

Cypress encourages global custom commands. Playwright doesn’t have a global command registry – instead, you create helper functions or use page objects.

// Cypress – support/commands.js
Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login');
  cy.get('#email').type(email);
  cy.get('#password').type(password);
  cy.get('button[type=submit]').click();
});

// Usage in test
cy.login('user@example.com', 'password123');
// Playwright – helper function (e.g., helpers/login.js)
export async function login(page, email, password) {
  await page.goto('/login');
  await page.locator('#email').fill(email);
  await page.locator('#password').fill(password);
  await page.locator('button[type=submit]').click();
}

// Usage in test
import { login } from './helpers/login';
test('authenticated user can access dashboard', async ({ page }) => {
  await login(page, 'user@example.com', 'password123');
  // ... rest of test
});

For broader sharing, wrap these helpers in a class (Page Object Model) and instantiate it in tests, or use Playwright’s built-in test.use() to provide a fixture.

Working with Cookies, LocalStorage, and Session

// Cypress
cy.setCookie('token', 'abc123');
cy.getCookie('token').should('exist');
cy.clearCookies();

// Playwright
await page.context().addCookies([{ name: 'token', value: 'abc123', domain: 'localhost', path: '/' }]);
const cookies = await page.context().cookies();
await page.context().clearCookies();

Multi-tab and Cross-origin

Cypress requires cy.origin() for cross-origin and doesn’t support multiple tabs. Playwright handles them natively:

// Open a new tab and switch
const newPage = await context.newPage();
await newPage.goto('https://other-origin.com');
// Continue interacting with both pages

5. Convert API Testing (cy.request → APIRequestContext)

Cypress uses cy.request() for API calls. Playwright offers a dedicated APIRequestContext that can be used independently or within browser tests to set up state.

// Cypress
cy.request('POST', '/api/users', { name: 'John' }).then((response) => {
  expect(response.status).to.eq(201);
});

// Playwright – inside a test using the built-in request fixture
test('create user via API', async ({ request }) => {
  const response = await request.post('/api/users', {
    data: { name: 'John' },
  });
  expect(response.status()).toBe(201);
});

This request fixture is isolated from the browser context, so it’s perfect for pure API testing or seeding data before a UI test.

6. Migrate CI/CD Pipeline

Update your CI configuration to run Playwright instead of (or alongside) Cypress. Here’s a GitHub Actions example:

# .github/workflows/playwright.yml
name: Playwright Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test

Playwright’s parallel execution (controlled via workers in config) often finishes faster than Cypress’s Dashboard-based parallelization, reducing CI minutes significantly.

Best Practices for a Smooth Migration

Conclusion

Migrating from Cypress to Playwright is a strategic upgrade that unlocks faster, more reliable, and truly cross-browser end-to-end testing. By understanding the mapping between Cypress commands and Playwright APIs, converting configuration, and restructuring custom commands into reusable helpers, you can transition incrementally with minimal disruption. The immediate gains – native parallel execution, multi-tab handling, mobile emulation, and a dedicated API testing layer – often justify the effort many times over. Start with a pilot, keep both frameworks running until confidence builds, and embrace Playwright’s modern debugging tools. Your test suite will become leaner, faster, and far more capable.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles