test-migration

Migrate a test suite from one framework to another, incrementally and without losing coverage. Covers Selenium→Playwright, Cypress→Playwright, Jest→Vitest, Mocha→Vitest, and Protractor→Playwright, with parallel CI running, locator/assertion translation, and a coverage-parity track. Use when: "migrat

By petrkindlmann · 608 installs

npx skills add petrkindlmann/qa-skills --skill test-migration

Source repository · Upstream listing

<objective A framework migration is where coverage quietly leaks: twenty old tests become fifteen new ones, a flaky Selenium test is faithfully reproduced as a flaky Playwright test, and the old safety net suite gets deleted a sprint too early. This skill migrates incrementally with both suites running in CI until parity is proven — so the new suite earns the decommission, you don't just assert it. </objective Quick Route You're going from → to Jump to First pass tooling Selenium → Playwright Translation Patterns + references/framework guides.md hand write; no codemod worth trusting Cypress → Playwright Translation Patterns + references/framework guides.md cy2pw web converter / community CLI (see tooling table) Protractor → Playwright references/framework guides.md (urgent — EOL 2023) hand write; map by.model / by.binding Jest → Vitest references/framework guides.md jest. → vi. sed pass, then verify Mocha → Vitest references/framework guides.md chai matcher codemod, then verify Any path, 100+ tests Migration Workflow + Parallel Running Strategy always parallel run in CI Discovery Questions Check .agents/qa project context.md first. If it exists, use it as context and skip questions already answered there. Current state: What framework are you migrating from and to? (drives the translation tables and which guide section applies) How many tests exist, and what is the current flakiness/skip rate? (migrating a flaky test reproduces the flakiness; skipped tests may not be worth migrating) Is there a coverage number today? (baseline for the parity track) Test infrastructure: What supporting infrastructure exists? (page objects, custom commands, fixtures, data factories — these migrate before tests) How is auth/session handled? (login flows are the most common silent breakage point — see Failure Modes) What CI pipeline runs the tests, and can both frameworks run simultaneously? (parallel run is non negotiable) Constraints: Timeline pressure? (urgent = framework EOL like Protractor; comfortable = modernization) Budget to run both suites during migration? (double CI cost, temporarily) Team's familiarity with the target framework? (training gap drives the workshop/pairing plan) Core Principles 1. Incremental over big bang A big bang rewrite — all tests at once — is the highest risk approach. It freezes test development for weeks, lands a large batch of unproven tests in one drop, and removes the proven safety net before the replacement exists. Always migrate one test, one module at a time. 2. Parallel run until parity Run both suites in CI until the new one provides at least the coverage of the old one. The old suite is your safety net; keep it non blocking but present. Do not decommission until the new suite has caught real regressions over multiple sprints. 3. Migrate highest value tests first Start with critical user journeys, frequently failing tests (which benefit most from a better framework), and high risk areas. Save low value tests for last — some are not worth migrating at all, and that's a valid, documented decision. 4. Modernize patterns during migration, don't just translate Translating a bad Selenium test into a bad Playwright test wastes the opportunity. For each test ask "how would I write this from scratch in the target framework?" — user facing locators, auto waiting, fixtures. This applies tenfold to AI codemod output (see Anti Patterns). Migration Workflow Six phases for a typical multi sprint migration. Use the checklists inline; heavy code lives in references. Phase 1 — Audit existing suite (Week 1). Count tests by category, skipped/flaky percentage, coverage if measured, runtime, page object/utility files, custom plugins, CI stages. Then categorize each test: Critical (revenue flows) → High (core journeys) → Medium (secondary) → Low (admin/edge) → Skip (disabled, duplicate, obsolete). This priority order drives everything downstream. Phase 2 — Set up target framework (Week 1 2). Install alongside the old one. Create the config ( playwright.config.ts / vitest.config.ts ), a separate test directory, a non blocking CI stage, one smoke test to prove the setup, reporters matching the existing format, shared env/secrets. Phase 3 — Migrate shared infrastructure (Week 2 3). Infrastructure before tests, in this order: base page object/test base → auth helpers → API client helpers → common page objects (nav/header/footer) → data factories → custom assertions → feature specific page objects (with their tests). Capture storageState here so migrated tests skip the login flow. Phase 4 — Migrate tests by priority (Week 3 8+). Per test: read the old one and understand what it actually verifies → write the new one from scratch with modern patterns (do not line by line translate) → run locally green → run in CI green → tag the old one "migrated" (don't delete) → after one sprint of parallel passing, delete the old one. Phase 5 — Parallel run in CI (throughout). Both suites run every pipeline. The legacy suite stays non blocking ( continue on error: true ) until the new suite reaches parity; then flip blocking onto the new suite and remove the old job. See references/parallel ci.md for the full GitHub Actions workflow. Phase 6 — Decommission old framework (final). Only after parity + stability: all critical/high tests migrated, new suite green in CI for 2+ consecutive sprints, new suite flakiness ≤ old, coverage comparison shows no regression, team writing new tests in the new framework for 2+ sprints. Then remove old deps from package.json , delete old test files (not just disable), update CI to run only the new suite, update docs. Automated Migration Tooling There is no AI magic button. Pick the right first pass tool by path, then refine by hand using references/framework guides.md . Tool Use when Don't trust for cy2pw web converter ([demo.playwright.dev/cy2pw](https://demo.playwright.dev/cy2pw/)) Cypress→Playwright, straightforward specs; official, deterministic, browser UI custom commands, POM conventions, fixture setup @11joselu/cypress to playwright (community CLI, npx @11joselu/cypress to playwright <dir ) Cypress→Playwright bulk first pass over a directory anything timing dependent — review every file AI agents (Claude Code, Cursor) custom command and fixture translation, the parts converters can't do a finished test — treat output as a first pass only Playwright 1.59+ agentic CLI ( npx playwright trace , debug=cli , AI optimized a11y snapshots) post migration triage of a failing migrated test the migration itself — these are debug tools, not converters Avoid: npx playwright migrate — there is no such built in Playwright CLI command; the instruction fails at the terminal (verified June 2026). Use cy2pw or the community CLI above. Golden reference recipe. Before letting any AI or codemod batch the suite, hand migrate ONE representative test end to end. Commit it as the team's canonical pattern (e.g. e2e/ golden/login.spec.ts ). Feed it back to the AI as a few shot reference and point human reviewers at it. Every later migration anchors to this file — it's the single highest leverage tactic for keeping AI output and reviewers consistent. Translation Patterns Locator mapping Old Pattern New Pattern (Playwright) Notes By.id('submit btn') page.getByRole('button', { name: 'Submit' }) Prefer role based By.css('.nav item.active') page.getByRole('link', { name: 'Dashboard' }) Use user visible text By.xpath('//div[@class="modal"]') page.getByRole('dialog') ARIA roles are more stable By.css('[data testid="user menu"]') page.getByTestId('user menu') testid as fallback cy.get('.product card').first() page.getByRole('article').first() Semantic elements preferred cy.contains('Add to cart') page.getByRole('button', { name: 'Add to cart' }) Specific role is better element(by.model('username')) page.getByLabel('Username') Angular model → label Role/label names above are illustrative — replace them with the accessible name your app actually renders. Wait strategy mapping Old Pattern New Pattern (Playwright) Notes Thread.sleep(3000) (remove entirely) Playwright auto waits WebDriverWait(driver, 10).until(visible) (remove entirely) Auto wait on actions cy.wait(2000) (remove entirely) Auto wait on assertions cy.wait('@apiCall') page.waitForResponse(/\/api\/data/) Explicit network wait (start the promise before the action) browser.wait(EC.presenceOf(...)) await expect(locator).toBeVisible() Web first assertion implicitlyWait(10, SECONDS) (remove — configure in config) Use actionTimeout in config FluentWait with polling await expect(locator).toHaveText('Done') Web first assertions retry Assertion mapping Old Pattern New Pattern (Playwright) Notes assert element.is displayed() await expect(locator).toBeVisible() Auto retrying cy.get('.msg').should('have.text', 'Done') await expect(locator).toHaveText('Done') Auto retrying expect(element.getText()).toBe('Done') await expect(locator).toHaveText('Done') Auto retrying cy.url().should('include', '/dashboard') await expect(page).toHaveURL(/dashboard/) Auto retrying assert len(elements) == 5 await expect(locator).toHaveCount(5) Auto retrying Config mapping (Cypress → Playwright) Old (Cypress) New (Playwright) baseUrl use.baseURL defaultCommandTimeout: 10000 use.actionTimeout: 10000 pageLoadTimeout: 30000 use.navigationTimeout: 30000 retries: { runMode: 2 } retries: 2 video: true use.video: 'on' screenshotOnRunFailure: true use.screenshot: 'only on failure' Specific Migration Guides Each path has its own key differences, before/after code, and migration notes checklist in references/framework guides.md . Quick orientation: Selenium → Playwright: Drop all explicit waits (Playwright auto waits), swap string locators for getByRole / getByLabel / getByTestId , replace WebDriver sessions with BrowserContext . Capture storageState instead of re implementing login. Jest → Vitest (target Vitest 4.x): Mostly API compatible — replace jest. with vi. , convert jest.config.js to vitest.config.ts , drop Babel/ts jest transforms (but keep an esbuild/SWC equivalent if you use custom Babel plugins like emotion/styled components macros). Expect 2 10x faster runs. Use Vitest 4.1 test tags for incremental cutover. Cypress → Playwright (target PW ≥ 1.50): The big shift is command queue → async/await. cy.intercept() + cy.wait() becomes page.route() + page.waitForResponse() ; custom commands become fixtures. Mind the 1.52 page.route() glob and Cookie header breaking changes (set cookies via browserContext.addCookies() , not a header override). Mocha → Vitest: Near 1:1. describe / it /hooks are unchanged; convert chai matchers ( to.equal → toBe , to.deep.equal → toEqual , to.contain → toContain ) and sinon → vi.fn() / vi.spyOn() . Protractor → Playwright: EOL since 2023 — urgent. Remove waitForAngular , map by.model / by.binding to getByLabel / getByText / getByTestId , and onPrepare becomes globalSetup . Parallel Running Strategy Coverage comparison during migration Track parity between old and new suites in a spreadsheet so nothing leaks. This artifact is the objective check at decommission time. Gradual cutover timeline For a 200 test suite, plan ~10 sprints: setup + infrastructure (Sprint 1 2), critical path migration (Sprint 3 5, ~50 tests), bulk migration (Sprint 6 8), cleanup + decommission (Sp