npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pw2cy

v1.1.0

Published

Production-quality CLI to migrate Playwright tests to Cypress specs using AST transforms

Readme

pw2cy — Playwright → Cypress Test Converter

Migrate your Playwright test suite to Cypress specs in minutes, not weeks.

pw2cy does the heavy lifting via AST transforms — no regex hacks — preserving test structure, names, and assertions while emitting clean, formatted, readable Cypress specs with a detailed HTML + JSON migration report.


Why pw2cy?

Cypress is the dominant end-to-end framework for many teams, but switching from Playwright means manually rewriting hundreds of test files. No official tooling exists for this migration:

| Problem | pw2cy solution | |---|---| | Manual page.*cy.* rewrites | AST-based transforms, not regexes | | Broken locator strategies | Maps all getBy* helpers to Cypress equivalents | | Different assertion APIs | Converts all expect().toXxx() chains | | Unknown unsupported APIs | Emits TODO comments with guidance | | No migration visibility | JSON + sortable HTML report with confidence scores | | TS + JS both needed | Handles .ts and .js inputs, preserves extension |


Installation

You can use pw2cy without even installing it, or set it up as a tool in your project.

Option 1: Zero-Install (Recommended)

Use npx to run it directly from the registry:

npx pw2cy init
npx pw2cy convert <input> --out <output>

Option 2: Global Installation

Install it permanently on your machine:

npm install -g pw2cy
# Now you can use the command directly
pw2cy convert ./tests --out ./cypress/e2e

Option 3: Local Dev Dependency

Add it to your project's package.json to lock the version:

npm install --save-dev pw2cy

Quick Start

# 1. Scaffold config and Cypress compat layer
npx pw2cy init

# 2. Convert a whole directory
npx pw2cy convert ./playwright-tests --out ./cypress/e2e

# 3. Convert a single file
npx pw2cy convert ./tests/login.spec.ts --out ./cypress/e2e

# 4. Dry run (no files written)
npx pw2cy convert ./playwright-tests --out ./cypress/e2e --dry-run

# 5. Print the migration report
npx pw2cy report

After running init, import the compat layer in cypress/support/e2e.ts:

import './pw-compat';

CLI Commands

pw2cy init

Scaffolds two files:

| File | Purpose | |---|---| | pw2cy.config.json | Configuration file with all options | | cypress/support/pw-compat.ts | Custom Cypress commands mirroring Playwright helpers | | cypress/tsconfig.json | TypeScript configuration for Cypress specs |

Options:

  • --force — Overwrite existing files
  • --no-compat — Skip compat layer generation

pw2cy convert <input> [options]

Converts Playwright specs to Cypress specs.

| Flag | Default | Description | |---|---|---| | --out <dir> | ./cypress/e2e | Output directory | | --config <file> | ./pw2cy.config.json | Config file path | | --report-dir <dir> | . | Where reports are written | | --dry-run | false | Parse & analyse without writing | | --verbose | false | Print detailed logs |

pw2cy report

Pretty-prints the last conversion report and optionally opens the HTML report.

| Flag | Default | Description | |---|---|---| | --json <file> | ./pw2cy-report.json | JSON report path | | --no-open | false | Don't auto-open HTML in browser |


Configuration (pw2cy.config.json)

{
  "useCompatLayer": true,
  "selectorPreference": ["data-testid", "role", "text", "css"],
  "baseUrl": "https://app.example.com",
  "replacePageGotoWithBaseUrl": true,
  "unsupportedBehavior": "comment",
  "assertionStyle": "should"
}

| Option | Type | Default | Description | |---|---|---|---| | useCompatLayer | boolean | true | Use cy.pwTestId/pwRole/pwText from compat layer | | selectorPreference | string[] | ["data-testid", ...] | Ordered locator strategy preference | | baseUrl | string? | "" | Base URL for goto() replacement | | replacePageGotoWithBaseUrl | boolean | false | Strip baseUrl prefix from visit() URLs | | unsupportedBehavior | "comment"\|"todo"\|"error" | "comment" | How to handle unrecognised APIs | | assertionStyle | "should"\|"expect" | "should" | Cypress assertion style |


Supported Conversions

Test Structure

| Playwright | Cypress | |---|---| | import { test, expect } from '@playwright/test' | (removed) | | test('name', async ({ page }) => { }) | it('name', () => { }) | | test.describe('Suite', fn) | describe('Suite', fn) | | test.describe.only(...) | describe.only(...) | | test.describe.skip(...) | describe.skip(...) | | test.beforeEach(async ({ page }) => { }) | beforeEach(() => { }) | | test.afterEach(...) | afterEach(...) | | test.beforeAll(...) | before(...) | | test.afterAll(...) | after(...) | | test.skip(...) | it.skip(...) | | test.only(...) | it.only(...) | | await test.step('name', fn) | cy.log('Step: name') (+ inner logic) |

Page Object Model (POM) & Classes (New!)

pw2cy now provides deep support for refactoring Playwright Page Objects into Cypress-compatible classes.

| Playwright POM | Cypress Transformation | |---|---| | export class LoginPage { ... } | export class LoginPage { ... } | | readonly page: Page; | (removed) | | constructor(page: Page) { ... } | constructor() { ... } (params removed) | | this.page = page; | (removed) | | this.user = page.locator('#u') | this.user = cy.get('#u') | | async login(u: string) { ... } | login(u) { ... } (async & types stripped) | | await this.user.fill(u) | this.user.clear().type(u) | | await this.user.click() | this.user.click() | | import { ... } from './utils.spec' | import { ... } from './utils.cy' (links updated) |

Navigation

| Playwright | Cypress | |---|---| | await page.goto(url) | cy.visit(url) | | await page.reload() | cy.reload() | | await page.goBack() | cy.go('back') | | await page.goForward() | cy.go('forward') | | await page.screenshot() | cy.screenshot() | | await page.url() | cy.url() | | await page.title() | cy.title() | | await page.content() | cy.document().invoke('documentElement.outerHTML') |

Locators

| Playwright | Cypress (useCompatLayer=true) | Cypress (raw) | |---|---|---| | page.locator(sel) | cy.get(sel) | cy.get(sel) | | page.getByTestId('x') | cy.pwTestId('x') | cy.get('[data-testid="x"]') | | page.getByRole('btn', {name:'S'}) | cy.pwRole('btn', {name:'S'}) | cy.contains('btn', 'S') | | page.getByText('Hello') | cy.pwText('Hello') | cy.contains('Hello') | | page.getByLabel('Email') | cy.pwLabel('Email') | cy.get('[aria-label="Email"]') | | page.getByPlaceholder('x') | cy.pwPlaceholder('x') | cy.get('[placeholder="x"]') | | page.getByAltText('x') | cy.pwAltText('x') | cy.get('[alt="x"]') | | page.getByTitle('x') | cy.pwTitle('x') | cy.get('[title="x"]') | | locator.first() | .first() | same | | locator.last() | .last() | same | | locator.nth(n) | .eq(n) | same | | locator.filter({hasText:'x'}) | .contains('x') | same |

Actions

| Playwright | Cypress | |---|---| | await locator.click() | .click() | | await locator.dblclick() | .dblclick() | | await locator.fill('v') | .clear().type('v') | | await locator.type('v') | .type('v') | | await locator.press('Enter') | .type('{enter}') | | await page.keyboard.type('v') | cy.get('body').type('v') | | await locator.check() | .check() | | await locator.uncheck() | .uncheck() | | await locator.selectOption('x') | .select('x') | | await locator.focus() | .focus() | | await locator.clear() | .clear() | | await locator.hover() | .trigger('mouseover') | | await locator.scrollIntoViewIfNeeded() | .scrollIntoView() | | await locator.screenshot() | .screenshot() |

Assertions

| Playwright | Cypress | |---|---| | await expect(loc).toBeVisible() | cy.get(...).should('be.visible') | | await expect(loc).toBeHidden() | .should('not.be.visible') | | await expect(loc).toBeEnabled() | .should('be.enabled') | | await expect(loc).toBeDisabled() | .should('be.disabled') | | await expect(loc).toBeChecked() | .should('be.checked') | | await expect(loc).not.toBeChecked() | .should('not.be.checked') | | await expect(loc).toHaveText('x') | .should('have.text', 'x') | | await expect(loc).toContainText('x') | .should('contain.text', 'x') | | await expect(loc).toHaveValue('x') | .should('have.value', 'x') | | await expect(loc).toHaveAttribute('a','v') | .should('have.attr', 'a', 'v') | | await expect(loc).toHaveCount(n) | .should('have.length', n) | | await expect(page).toHaveURL(/re/) | cy.url().should('match', /re/) | | await expect(page).toHaveURL('str') | cy.url().should('eq', 'str') | | await expect(page).toHaveTitle('t') | cy.title().should('include', 't') |

Waits

| Playwright | Cypress | |---|---| | await page.waitForURL(url) | cy.url().should('include', url) | | await page.waitForSelector(sel) | cy.get(sel).should('exist') | | await page.waitForTimeout(ms) | cy.wait(ms) |


Compat Layer (pw-compat.ts)

The compat layer (cypress/support/pw-compat.ts) adds custom commands that keep converted specs readable without raw CSS selectors everywhere:

cy.pwTestId('submit-btn')           // → [data-testid="submit-btn"]
cy.pwRole('button', { name: 'Save' }) // → [role="button"]:contains('Save')
cy.pwText('Welcome back')           // → cy.contains('Welcome back')
cy.pwLocator('.my-class')           // → cy.get('.my-class')

The testIdAttribute can be customised via Cypress.env('testIdAttribute').


Migration Report

After every convert run, two reports are generated:

pw2cy-report.json — machine-readable:

{
  "generatedAt": "2024-01-15T12:00:00.000Z",
  "pw2cyVersion": "1.0.0",
  "totalFiles": 12,
  "totalConverted": 143,
  "totalTodos": 7,
  "averageConfidence": 94,
  "files": [...]
}

pw2cy-report.html — sortable HTML table with:

  • Per-file confidence score (0–100%)
  • Converted nodes count
  • TODO count per file
  • List of unsupported API calls
  • Dark-theme UI, sortable columns

Confidence score = converted / (converted + todos) * 100.


Limitations & How the Compat Layer Helps

| Limitation | Workaround | |---|---| | page.route() — no direct equivalent | Use cy.intercept() — TODO comment with guidance | | page.waitForLoadState('networkidle') | Cypress auto-waits; TODO comment inserted | | page.evaluate() | Use cy.window().then(win => ...) — TODO comment | | Complex locator chains | pw-compat layer keeps them clean; review .filter() TODOs | | Playwright fixtures (test.use, test.extend) | Must be refactored to Cypress plugins — TODO emitted | | ARIA role semantics | cy.pwRole uses [role=x] and :contains(name) — not a full ARIA tree |


Development

git clone https://github.com/your-org/pw2cy
cd pw2cy
npm install
npm run build
npm test

Running from source

npm run dev -- init
npm run dev -- convert ./tests/samples --out /tmp/cypress-out --verbose

Project structure

pw2cy/
├── src/
│   ├── cli.ts                 # Commander CLI entrypoint
│   ├── index.ts               # Programmatic API
│   ├── commands/
│   │   ├── init.ts            # pw2cy init
│   │   ├── convert.ts         # pw2cy convert
│   │   └── report.ts          # pw2cy report
│   ├── core/
│   │   ├── config.ts          # Zod schema + loader
│   │   ├── parser.ts          # Babel parser wrapper
│   │   ├── transform.ts       # AST transformer (main logic)
│   │   ├── mappings.ts        # Declarative conversion tables
│   │   └── report.ts          # JSON + HTML report generators
│   └── utils/
│       ├── fs.ts              # File system helpers
│       └── log.ts             # Chalk logger
├── templates/
│   ├── pw2cy.config.json      # Default config template
│   └── pw-compat.ts           # Cypress compat layer template
├── tests/
│   ├── transform.test.ts      # 35+ unit + snapshot tests
│   └── samples/               # 5 Playwright input samples
│       ├── login.spec.ts
│       ├── products.spec.ts
│       ├── form.spec.ts
│       ├── api.spec.ts
│       └── navigation.spec.ts
└── scripts/
    └── postbuild.js           # Adds shebang + copies templates

Contributing

We welcome contributions! To set up pw2cy for development:

  1. Fork and Clone the repository.
  2. Install Dependencies:
    npm install
  3. Build and Test:
    npm run build
    npm test
  4. Local Testing (Global Link): To test your changes against a real Playwright project, link your local version:
    # In the pw2cy root
    npm link
    
    # Now, in any other folder on your machine, 'pw2cy' will use your code
    pw2cy convert ./some-tests --out ./here
  5. Create a Branch:
    git checkout -b feat/your-awesome-feature
  6. Submit a PR with a clear description and tests.

Adding a new conversion rule

  1. Add the mapping to src/core/mappings.ts
  2. Handle the transformation in src/core/transform.ts
  3. Add a unit test in tests/transform.test.ts
  4. Update the supported conversions table in README

Roadmap (v2)

| Feature | Status | |---|---| | POM (Page Object Model) file conversion | ✅ Supported (v1.0.4) | | Smart Import Path Adjustment (.spec -> .cy) | ✅ Supported (v1.0.4) | | cy.intercept() full pattern for page.route() | Planned | | Playwright fixtures → Cypress fixtures/plugins | Planned | | page.evaluate()cy.window().then() | Planned | | GitHub Action for CI migration validation | Planned | | Config JSON schema file for IDE autocompletion | Planned | | --watch mode for incremental conversion | Planned | | Vue/React component test support | Research | | @playwright/experimental-ct-* support | Research |


License

MIT © pw2cy contributors