Framework migrations — Cypress to Playwright, Selenium to Playwright, JUnit 4 to JUnit 5, Mocha to Vitest — used to mean months of parallel work and constant context-switching between two syntaxes. AI assistance compresses the mechanical conversion dramatically, but the strategy matters as much as the tooling. This lesson covers how to migrate confidently without shipping a broken suite.
The pilot-first strategy
Never start with a bulk migration. Start with one representative test, get the conversion right, then use that as the template for everything else.
We are migrating from Cypress to Playwright.
Convert cypress/e2e/auth/login.cy.ts to Playwright.
Before writing any code, show me a mapping table:
- cy.visit → equivalent
- cy.get → equivalent
- cy.findByTestId → equivalent
- cy.intercept → equivalent
- cy.fixture → equivalent
- Custom commands from cypress/support/commands.ts → equivalent pattern
Then convert the file. Save to tests/auth/login.spec.ts.
Create src/pages/LoginPage.ts if the test needs a page object.The mapping table is the most valuable output here. It surfaces the conceptual mismatches (not just syntax differences) before you are committed to 80 files of conversions. Review it before approving the conversion.
Running the pilot immediately
After conversion, run the new test before converting anything else:
npx playwright test tests/auth/login.spec.tsIf it fails, fix it with Claude Code before proceeding. A pilot that does not pass teaches you something important about the conversion. A bulk migration of 80 files that then all fail teaches you the same thing, but expensively.
Bulk migration using the pilot as a template
Once the pilot passes:
Convert all remaining tests in cypress/e2e/auth/ to Playwright.
Follow the same patterns as the login.spec.ts conversion we just completed:
- Same page object structure
- Same wait patterns
- Same approach to cy.intercept → page.route
Save to tests/auth/. Show me a summary of files converted.By referencing the completed pilot, Claude applies consistent patterns across the batch rather than reinventing the approach for each file.
Common migration gotchas
Async behaviour differences. Cypress automatically retries commands and assertions. Playwright's auto-waiting applies to actions but not all assertions. Tests that relied on Cypress retries need explicit expect(...).toBeVisible() or waitFor conditions added.
Network interception syntax. cy.intercept('POST', '/api/orders', { fixture: 'order.json' }) becomes page.route('**/api/orders', route => route.fulfill({ path: 'tests/fixtures/order.json' })). Claude handles this conversion, but review each intercepted route — the route patterns differ.
Custom Cypress commands. Commands defined in cypress/support/commands.ts have no Playwright equivalent. Claude converts them to either fixtures (for setup/teardown), page object methods (for page interactions), or standalone helper functions. Ask it to show you the conversion plan before applying.
Test isolation. Cypress tests share browser state across the run by default. Playwright uses a fresh context per test by default. Some Cypress tests that relied on shared login state need page.context().storageState() fixtures added.
Framework upgrades within the same language
For upgrades that are mostly mechanical — JUnit 4 to JUnit 5, Mocha to Vitest — the conversion is easier:
Migrate tests/unit/ from Mocha + Chai to Vitest.
Key changes needed:
- import { describe, it, expect } from 'vitest' instead of requiring mocha/chai
- expect() syntax stays the same (Vitest is chai-compatible)
- done() callbacks → async/await
- beforeEach/afterEach stay the same
Convert tests/unit/pricing.test.ts first. Run it. Then bulk convert the rest.Vitest's compatibility layer means most Chai assertions migrate without change. The main work is import lines and async patterns.
Step 1 of 6
Set up the target framework
Install the new framework alongside the old one. Both coexist during migration — no big-bang switch.
⚠️ Common Mistakes
- Bulk migrating without a pilot. The pilot surfaces framework-specific gotchas before they propagate across 80 files. Skip it and you discover the systematic error at the end, not the beginning.
- Not running converted tests incrementally. "I'll convert everything and run it at the end" is how you end up with 50 failing tests and no clear signal about which conversions introduced which failures. Run each batch.
- Deleting the old framework too soon. Keep the old framework running alongside the new one until the full migration is complete and CI is stable. Having a working reference makes debugging conversions much easier.
🎯 Practice Task
Run a small framework migration. 30 minutes.
- Pick three to five tests from a project that could be migrated to a different framework or an upgraded version.
- Ask Claude Code to produce the mapping table for the key API differences.
- Review the mapping — are there any conversions that would need manual judgement?
- Migrate the first test using the pilot approach. Run it and verify it passes.
- Migrate the remaining two to four tests, referencing the pilot.
Chapter 4 moves from maintenance patterns to Claude Code's power features — CLAUDE.md, custom commands, MCP servers, and agentic workflows.