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:
- Single-tab, single-origin restriction – Cypress can’t easily handle multi-tab flows or cross-origin navigation (requires the new
cy.origincommand, which is still limited). - No native mobile emulation – Cypress doesn’t support touch events, geolocation, or mobile viewport testing out of the box.
- Limited parallelization – The Dashboard-based parallelization is tied to their cloud service and doesn’t scale as freely as open-source runners.
- No built-in API testing context –
cy.request()works, but lacks a dedicated, isolated API testing layer. - Browser support – Cypress historically only supported Chromium-based browsers and Firefox; Safari/WebKit testing was impossible without third-party services.
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:
- Cypress-specific commands:
cy.visit(),cy.get(),cy.intercept(),cy.fixture(), custom commands defined viaCypress.Commands.add(). - Plugins: Any plugins that hook into
cypress/plugins/index.js(or the newersetupNodeEventsin Cypress 10+). - Configuration dependencies:
cypress.json(orcypress.config.js) settings like baseUrl, viewport, retries, timeouts. - Test structure: Folder organization, fixtures, support files, page objects (if used).
- CI/CD integration: Existing Cypress GitHub Actions, Jenkins, or CircleCI scripts.
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:
cy.visit(url)→await page.goto(url)cy.get(selector)→page.locator(selector).type()→.fill()(Playwright also has.pressSequentially()for keyboard‑like typing).click()→.click()(both frameworks use the same method name).should('be.visible')→await expect(locator).toBeVisible()
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
- Start with a Pilot Suite – Migrate the most critical or flakiest tests first. Prove that Playwright solves specific pain points (flakiness, speed, cross-browser).
- Run Both Frameworks in Parallel – Keep the Cypress suite intact while you build Playwright coverage. Use a feature flag in CI to run both until Playwright reaches parity.
- Leverage Playwright’s Auto-waiting – Avoid explicit
waitForTimeoutor fixed sleeps. Playwright’s locators and actions automatically wait for elements to be actionable. This alone reduces flakiness. - Embrace Page Objects or Fixtures – Replace global custom commands with well-structured page object classes or Playwright’s
test.useto inject shared state. This improves maintainability. - Use Trace Viewer for Debugging – Instead of Cypress’s time-travel, use Playwright’s
--trace onflag. It captures DOM snapshots, network logs, and screenshots for every action, providing a powerful debugging experience. - Configure Retries and Workers Wisely – Playwright supports test-level retries and worker count. In CI, set
retries: 2and workers to the number of available cores minus one (or usefullyParallel: true). - Adopt Cross-Browser Testing Gradually – Once the core suite works on Chromium, add projects for Firefox and WebKit in
playwright.config.ts. This expands coverage without extra effort. - Don’t Migrate Legacy Cypress Plugins Blindly – Evaluate whether the plugin functionality is now built into Playwright (e.g., visual testing, component testing). Playwright offers native component testing (experimental) and can integrate with third-party visual tools like Percy or Chromatic.
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.