Migrate: Cypress to Playwright
Translate Cypress E2E tests into idiomatic Playwright code following Console's layered architecture. Adapted from openshift-ui-tests-template/migrate.md.
Before Starting
- Check that
frontend/e2e/.env exists. If missing, copy frontend/e2e/.env.example to frontend/e2e/.env and tell the user to fill in their cluster values before continuing.
- Read
.claude/e2e-context.md for project conventions, patterns, and rules, page objects, selectors, fixtures, cleanup, waits, and things to never do. That file is the single source of truth for how Playwright tests should be structured.
- Read
.claude/migration-context.md for the Cypress → Playwright API translation tables, structural transformation rules, Gherkin collapse mappings, and migration checklist.
Input
/migrate-cypress <cypress-file-or-feature> — full migration (analyze → implement → validate)
/migrate-cypress <cypress-file-or-feature> --analyze — analysis only, produce migration plan
/migrate-cypress <cypress-file-or-feature> --dry-run — generate code without writing files
Examples
/migrate-cypress frontend/packages/integration-tests/tests/cluster-settings/upstream-modal.cy.ts
/migrate-cypress upstream-modal --analyze
/migrate-cypress frontend/packages/dev-console/integration-tests/features/addFlow/ --dry-run
Output File Mapping
Migrated tests go under e2e/tests/<package>/ based on their source package:
| Source package |
Playwright project |
Output directory |
packages/integration-tests/tests/<area>/ |
console |
e2e/tests/console/<area>/ |
packages/dev-console/integration-tests/ |
dev-console |
e2e/tests/dev-console/ |
packages/helm-plugin/integration-tests/ |
helm |
e2e/tests/helm/ |
packages/knative-plugin/integration-tests/ |
knative |
e2e/tests/knative/ |
packages/operator-lifecycle-manager/integration-tests/ |
olm |
e2e/tests/olm/ |
packages/topology/integration-tests/ |
topology |
e2e/tests/topology/ |
packages/webterminal-plugin/integration-tests/ |
webterminal |
e2e/tests/webterminal/ |
packages/console-telemetry-plugin/integration-tests/ |
telemetry |
e2e/tests/telemetry/ |
Tests requiring developer auth go in a developer/ subdirectory (e.g. e2e/tests/dev-console/developer/).
Workflow
Phase 1: Analysis
- Read the Cypress file and all imported views, constants, types, and custom commands. For
.feature files, also resolve step definitions (support/step-definitions/), page actions (support/pages/), and page object selectors (support/pageObjects/).
- Extract intent — document what each
it block or Scenario tests in plain language
- Search existing page objects and clients for reusable methods (from project root):
find frontend/e2e/pages frontend/e2e/clients -name "*.ts" 2>/dev/null
- Identify gaps — missing locators, page object methods
- Determine test isolation strategy (self-contained, shared resources, or API-created — see migration-context.md for details)
- Produce a migration plan mapping Cypress blocks to Playwright components
Expected output: a structured plan listing each test's intent, the Playwright components to use/create, the isolation strategy, and the output file path.
Stop here if --analyze was specified.
Phase 2: Selector Discovery (Playwright MCP)
- Resize viewport to 1920×1080
- Navigate to target pages in the live UI
- Snapshot accessibility tree to discover
data-test attributes and element roles
- Verify selectors with non-submitting interactions (click navigation elements, type in search fields). Do not submit forms or perform create/update/delete actions during discovery. Ask the user before login or credential entry
- Update migration plan with verified selectors
If MCP is unavailable or no cluster is reachable, log a warning: "Playwright MCP not available — selectors translated literally. They may be stale. Run /debug-test after deployment to verify." Proceed to Phase 3.
Phase 3: Implementation
- Create/extend page objects with locators and interaction methods. Follow the established pattern:
- Import
Locator type from @playwright/test and default-import BasePage
- Use
getByTestId() for data-test attributes, locator() for other selectors
- If the React component only has a legacy test attribute (
data-test-id, data-test-rows, data-test-dropdown-menu, etc.) but no data-test, add data-test to the React component source and use getByTestId() — never use legacy attribute selectors directly
- Expose locators via getter methods (
getX(): Locator), keep locator properties private readonly
- Use
robustClick() inside page objects; specs use plain .click()
- Do NOT add
waitFor() before action methods (fill(), click(), check()) — Playwright auto-waits for actionability
- Do NOT name methods or locators with a
legacy prefix — name for what they do
Example:
// e2e/pages/cluster-settings.ts
import type { Locator } from "@playwright/test";
import BasePage from "./base-page";
export class ClusterSettingsPage extends BasePage {
private readonly detailsTab = this.page.getByTestId("horizontal-link-Details");
private readonly pageHeading = this.page.getByTestId("cluster-settings-page-heading");
async navigateToDetails(): Promise<void> {
await this.goTo("/settings/cluster");
await this.detailsTab.waitFor({ state: "visible" });
}
getPageHeading(): Locator {
return this.pageHeading;
}
}
- Write the spec file following project template:
import { test, expect } from '../../fixtures'
- Tags are optional. Only add them if they enable filtering beyond the directory structure (see
e2e-context.md Tags section)
- Admin tests go in
e2e/tests/<package>/, developer tests in e2e/tests/<package>/developer/
- Self-contained: create → assert → cleanup in each
test()
- Use
test.step() for logical grouping when a test has 3+ distinct phases
- For Gherkin
Scenario Outline + Examples: use for...of loop
- For
@manual / @broken-test: use test.skip(true, 'reason') or test.fixme('reason') with Jira link
- Run
npx tsc --noEmit -p e2e/tsconfig.json and cd frontend && yarn eslint <generated-files> — fix any type or lint errors
Print code without writing if --dry-run was specified.
Phase 4: Validation
- Run with
npx playwright test --project=<package> <output-file> --retries=0. The project name matches the package directory under e2e/tests/ (e.g. --project=helm, --project=console). For developer tests, use --project=<package>-developer. Add --ui only if the user requests interactive debugging. Note: e2e/.env may override WEB_CONSOLE_URL with a remote cluster URL. If running against localhost, ensure the user has updated e2e/.env or override with WEB_CONSOLE_URL=http://localhost:9000.
- If the first run passes, run 2 additional times with
--retries=0 to catch intermittent failures. A migrated test that passes once but fails on subsequent runs is flaky and must be fixed before the migration is complete.
- Debug failures using Playwright MCP (navigate → snapshot → console → network). Fix and re-run.
- If a test still fails after 3 fix attempts, stop trying and ask the user if they want to run
/debug-test <spec-file> for deeper MCP-assisted diagnosis.
- Verify no orphaned resources after run
- Produce migration summary:
Migration complete: <source-file> → <output-file>
Tests migrated: N
Page objects created: [list]
Page objects reused: [list]
Files written: [list]
Validation: passed
Test mapping:
| Cypress test | Playwright test | Assertions |
|---------------------------------------|---------------------------------------|------------|
| it('does something') | test('does something') | 3 → 3 |
| it('handles edge case') | test('handles edge case') | 2 → 2 |
| ... | ... | ... |
| Total | | 8 → 8 |
The "Assertions" column shows <cypress count> → <playwright count>. If a Playwright test has fewer assertions than its Cypress counterpart, flag it with ⚠ and investigate — assertions may have been silently dropped.
- If validation passes, delete the original Cypress test file. Also check imported view and support files — if they are no longer imported by any other Cypress test, delete them too.
Flakiness Prevention
Migration is the biggest source of flaky tests. Cypress and Playwright have fundamentally different execution models, and patterns that are stable in Cypress can be intermittent in Playwright. Watch for these during migration:
- Cypress retries assertions implicitly.
cy.get(s).should('be.visible') retries for up to 4 seconds by default. Playwright's expect(locator).toBeVisible() also retries, but with a different timeout (5s default). If the Cypress test relied on a long implicit wait, pass an explicit timeout: await expect(locator).toBeVisible({ timeout: 30_000 }).
- Cypress
cy.wait() masks timing issues. A cy.wait(3000) in Cypress often hides a race condition. Don't replace it with page.waitForTimeout(3000). Instead, find what the wait is actually waiting for (a network response, a DOM element, a state change) and wait for that condition explicitly.
- Cypress shared state hides flakes. With
testIsolation: false, sequential it blocks share cookies, DOM, and navigation state. A test that passes in Cypress because the previous it left the page in the right state will fail intermittently in Playwright if the navigation or setup is incomplete. When merging it blocks into test.step(), make each step's preconditions explicit.
- PatternFly overlays intercept clicks. Cypress
click({ force: true }) bypasses overlay checks. In Playwright, use robustClick() in page objects. This retries with scroll-into-view and falls back to force-click, which is more reliable than a single forced click.
- Resource creation races. Cypress
cy.exec('oc create ...') is synchronous within the test. k8sClient.createNamespace() is async. If the test navigates to a resource page immediately after creation, add an explicit wait for the resource to be ready: await k8sClient.waitForNamespaceReady(ns).
Key Translation Rules
- Never transliterate — understand intent, use idiomatic Playwright APIs
- Self-contained tests — merge sequential
it blocks into one test() with test.step()
- No fixed waits — replace
cy.wait(ms) with condition-based waits or assertion timeouts
- No redundant
waitFor() — fill(), click(), check(), etc. auto-wait for actionability; only use waitFor() when waiting for state without acting on the element
- No shell commands — replace
cy.exec('oc ...') with KubernetesClient
- No try/catch in cleanup —
k8sClient.deleteNamespace(), deleteCustomResource(), and deleteClusterCustomResource() already swallow 404 errors
- Add
data-test to React source — when the component only has legacy test attributes (data-test-id, data-test-rows, etc.), add data-test alongside and use getByTestId()
- Framework-first — use existing page objects before creating new ones
- Correct layer — locators in page objects, test scenarios in specs; common multi-step interactions belong in page object methods, not inline in specs
Troubleshooting
Playwright MCP not connected
If MCP tools fail with "tool not found" or "connection refused": skip Phase 2, translate selectors literally, and warn the user. Selectors can be verified later with /debug-test.
TypeScript errors in generated files
If npx tsc --noEmit reports errors in the generated spec or page object: read the errors, fix missing imports or type mismatches, and re-run. Common causes: missing page object import, wrong fixture type, incorrect KubernetesClient method signature.
Test passes but assertions are missing
Compare assertion count with the original Cypress test. If the migrated test has fewer assertions, the agent may have silently dropped failing ones. Restore them using the intent documented in Phase 1.
Rules
- Always read the Cypress source before writing any code
- Use Playwright MCP to verify selectors against the live UI
- Follow
.claude/e2e-context.md for Playwright patterns and .claude/migration-context.md for Cypress translation tables
- DO NOT commit — the user handles git operations
1---2name: migrate-cypress3description: Migrate a Cypress test file (.cy.ts) or Gherkin feature file (.feature) to Playwright following Console's architecture. This is the ONLY skill for Cypress-to-Playwright conversion work, supporting full migration, analysis-only (--analyze), or dry-run (--dry-run) modes. Use this skill whenever the user wants to convert, port, rewrite, or migrate Cypress or Gherkin tests to Playwright, mentions "old cypress tests", "remaining e2e tests", or provides a .cy.ts or .feature file path in a migration context.4---56# Migrate: Cypress to Playwright78Translate Cypress E2E tests into idiomatic Playwright code following Console's layered architecture. Adapted from [openshift-ui-tests-template/migrate.md](https://github.com/bmaio-redhat/openshift-ui-tests-template/blob/main/.cursor/commands/migrate.md).910## Before Starting11121. Check that `frontend/e2e/.env` exists. If missing, copy `frontend/e2e/.env.example` to `frontend/e2e/.env` and tell the user to fill in their cluster values before continuing.132. Read `.claude/e2e-context.md` for project conventions, patterns, and rules, page objects, selectors, fixtures, cleanup, waits, and things to never do. That file is the single source of truth for how Playwright tests should be structured.143. Read `.claude/migration-context.md` for the Cypress → Playwright API translation tables, structural transformation rules, Gherkin collapse mappings, and migration checklist.1516## Input1718- `/migrate-cypress <cypress-file-or-feature>` — full migration (analyze → implement → validate)19- `/migrate-cypress <cypress-file-or-feature> --analyze` — analysis only, produce migration plan20- `/migrate-cypress <cypress-file-or-feature> --dry-run` — generate code without writing files2122### Examples2324```25/migrate-cypress frontend/packages/integration-tests/tests/cluster-settings/upstream-modal.cy.ts26/migrate-cypress upstream-modal --analyze27/migrate-cypress frontend/packages/dev-console/integration-tests/features/addFlow/ --dry-run28```2930### Output File Mapping3132Migrated tests go under `e2e/tests/<package>/` based on their source package:3334| Source package | Playwright project | Output directory |35| -------------------------------------------------------- | ------------------ | --------------------------- |36| `packages/integration-tests/tests/<area>/` | `console` | `e2e/tests/console/<area>/` |37| `packages/dev-console/integration-tests/` | `dev-console` | `e2e/tests/dev-console/` |38| `packages/helm-plugin/integration-tests/` | `helm` | `e2e/tests/helm/` |39| `packages/knative-plugin/integration-tests/` | `knative` | `e2e/tests/knative/` |40| `packages/operator-lifecycle-manager/integration-tests/` | `olm` | `e2e/tests/olm/` |41| `packages/topology/integration-tests/` | `topology` | `e2e/tests/topology/` |42| `packages/webterminal-plugin/integration-tests/` | `webterminal` | `e2e/tests/webterminal/` |43| `packages/console-telemetry-plugin/integration-tests/` | `telemetry` | `e2e/tests/telemetry/` |4445Tests requiring developer auth go in a `developer/` subdirectory (e.g. `e2e/tests/dev-console/developer/`).4647## Workflow4849### Phase 1: Analysis50511. Read the Cypress file and all imported views, constants, types, and custom commands. For `.feature` files, also resolve step definitions (`support/step-definitions/`), page actions (`support/pages/`), and page object selectors (`support/pageObjects/`).522. Extract intent — document what each `it` block or `Scenario` tests in plain language533. Search existing page objects and clients for reusable methods (from project root): `find frontend/e2e/pages frontend/e2e/clients -name "*.ts" 2>/dev/null`544. Identify gaps — missing locators, page object methods555. Determine test isolation strategy (self-contained, shared resources, or API-created — see migration-context.md for details)566. Produce a migration plan mapping Cypress blocks to Playwright components5758Expected output: a structured plan listing each test's intent, the Playwright components to use/create, the isolation strategy, and the output file path.5960**Stop here if `--analyze` was specified.**6162### Phase 2: Selector Discovery (Playwright MCP)63641. Resize viewport to 1920×1080652. Navigate to target pages in the live UI663. Snapshot accessibility tree to discover `data-test` attributes and element roles674. Verify selectors with non-submitting interactions (click navigation elements, type in search fields). Do not submit forms or perform create/update/delete actions during discovery. Ask the user before login or credential entry685. Update migration plan with verified selectors6970If MCP is unavailable or no cluster is reachable, log a warning: "Playwright MCP not available — selectors translated literally. They may be stale. Run `/debug-test` after deployment to verify." Proceed to Phase 3.7172### Phase 3: Implementation73741. Create/extend page objects with locators and interaction methods. Follow the established pattern:75 - Import `Locator` type from `@playwright/test` and default-import `BasePage`76 - Use `getByTestId()` for `data-test` attributes, `locator()` for other selectors77 - If the React component only has a legacy test attribute (`data-test-id`, `data-test-rows`, `data-test-dropdown-menu`, etc.) but no `data-test`, **add `data-test` to the React component source** and use `getByTestId()` — never use legacy attribute selectors directly78 - Expose locators via getter methods (`getX(): Locator`), keep locator properties `private readonly`79 - Use `robustClick()` inside page objects; specs use plain `.click()`80 - Do NOT add `waitFor()` before action methods (`fill()`, `click()`, `check()`) — Playwright auto-waits for actionability81 - Do NOT name methods or locators with a `legacy` prefix — name for what they do8283Example:84 ```typescript85 // e2e/pages/cluster-settings.ts86 import type { Locator } from "@playwright/test";87 import BasePage from "./base-page";8889 export class ClusterSettingsPage extends BasePage {90 private readonly detailsTab = this.page.getByTestId("horizontal-link-Details");91 private readonly pageHeading = this.page.getByTestId("cluster-settings-page-heading");9293 async navigateToDetails(): Promise<void> {94 await this.goTo("/settings/cluster");95 await this.detailsTab.waitFor({ state: "visible" });96 }9798 getPageHeading(): Locator {99 return this.pageHeading;100 }101 }102 ```1032. Write the spec file following project template:104 - `import { test, expect } from '../../fixtures'`105 - Tags are optional. Only add them if they enable filtering beyond the directory structure (see `e2e-context.md` Tags section)106 - Admin tests go in `e2e/tests/<package>/`, developer tests in `e2e/tests/<package>/developer/`107 - Self-contained: create → assert → cleanup in each `test()`108 - Use `test.step()` for logical grouping when a test has 3+ distinct phases109 - For Gherkin `Scenario Outline` + `Examples`: use `for...of` loop110 - For `@manual` / `@broken-test`: use `test.skip(true, 'reason')` or `test.fixme('reason')` with Jira link1113. Run `npx tsc --noEmit -p e2e/tsconfig.json` and `cd frontend && yarn eslint <generated-files>` — fix any type or lint errors112113**Print code without writing if `--dry-run` was specified.**114115### Phase 4: Validation1161171. Run with `npx playwright test --project=<package> <output-file> --retries=0`. The project name matches the package directory under `e2e/tests/` (e.g. `--project=helm`, `--project=console`). For developer tests, use `--project=<package>-developer`. Add `--ui` only if the user requests interactive debugging. Note: `e2e/.env` may override `WEB_CONSOLE_URL` with a remote cluster URL. If running against localhost, ensure the user has updated `e2e/.env` or override with `WEB_CONSOLE_URL=http://localhost:9000`.1182. If the first run passes, run 2 additional times with `--retries=0` to catch intermittent failures. A migrated test that passes once but fails on subsequent runs is flaky and must be fixed before the migration is complete.1193. Debug failures using Playwright MCP (navigate → snapshot → console → network). Fix and re-run.1204. If a test still fails after 3 fix attempts, stop trying and ask the user if they want to run `/debug-test <spec-file>` for deeper MCP-assisted diagnosis.1215. Verify no orphaned resources after run1226. Produce migration summary:123 ```124 Migration complete: <source-file> → <output-file>125 Tests migrated: N126 Page objects created: [list]127 Page objects reused: [list]128 Files written: [list]129 Validation: passed130131 Test mapping:132 | Cypress test | Playwright test | Assertions |133 |---------------------------------------|---------------------------------------|------------|134 | it('does something') | test('does something') | 3 → 3 |135 | it('handles edge case') | test('handles edge case') | 2 → 2 |136 | ... | ... | ... |137 | Total | | 8 → 8 |138 ```139 The "Assertions" column shows `<cypress count> → <playwright count>`. If a Playwright test has fewer assertions than its Cypress counterpart, flag it with `⚠` and investigate — assertions may have been silently dropped.1405. If validation passes, delete the original Cypress test file. Also check imported view and support files — if they are no longer imported by any other Cypress test, delete them too.141142## Flakiness Prevention143144Migration is the biggest source of flaky tests. Cypress and Playwright have fundamentally different execution models, and patterns that are stable in Cypress can be intermittent in Playwright. Watch for these during migration:145146- **Cypress retries assertions implicitly.** `cy.get(s).should('be.visible')` retries for up to 4 seconds by default. Playwright's `expect(locator).toBeVisible()` also retries, but with a different timeout (5s default). If the Cypress test relied on a long implicit wait, pass an explicit timeout: `await expect(locator).toBeVisible({ timeout: 30_000 })`.147- **Cypress `cy.wait()` masks timing issues.** A `cy.wait(3000)` in Cypress often hides a race condition. Don't replace it with `page.waitForTimeout(3000)`. Instead, find what the wait is actually waiting for (a network response, a DOM element, a state change) and wait for that condition explicitly.148- **Cypress shared state hides flakes.** With `testIsolation: false`, sequential `it` blocks share cookies, DOM, and navigation state. A test that passes in Cypress because the previous `it` left the page in the right state will fail intermittently in Playwright if the navigation or setup is incomplete. When merging `it` blocks into `test.step()`, make each step's preconditions explicit.149- **PatternFly overlays intercept clicks.** Cypress `click({ force: true })` bypasses overlay checks. In Playwright, use `robustClick()` in page objects. This retries with scroll-into-view and falls back to force-click, which is more reliable than a single forced click.150- **Resource creation races.** Cypress `cy.exec('oc create ...')` is synchronous within the test. `k8sClient.createNamespace()` is async. If the test navigates to a resource page immediately after creation, add an explicit wait for the resource to be ready: `await k8sClient.waitForNamespaceReady(ns)`.151152## Key Translation Rules153154- **Never transliterate** — understand intent, use idiomatic Playwright APIs155- **Self-contained tests** — merge sequential `it` blocks into one `test()` with `test.step()`156- **No fixed waits** — replace `cy.wait(ms)` with condition-based waits or assertion timeouts157- **No redundant `waitFor()`** — `fill()`, `click()`, `check()`, etc. auto-wait for actionability; only use `waitFor()` when waiting for state without acting on the element158- **No shell commands** — replace `cy.exec('oc ...')` with `KubernetesClient`159- **No try/catch in cleanup** — `k8sClient.deleteNamespace()`, `deleteCustomResource()`, and `deleteClusterCustomResource()` already swallow 404 errors160- **Add `data-test` to React source** — when the component only has legacy test attributes (`data-test-id`, `data-test-rows`, etc.), add `data-test` alongside and use `getByTestId()`161- **Framework-first** — use existing page objects before creating new ones162- **Correct layer** — locators in page objects, test scenarios in specs; common multi-step interactions belong in page object methods, not inline in specs163164## Troubleshooting165166### Playwright MCP not connected167168If MCP tools fail with "tool not found" or "connection refused": skip Phase 2, translate selectors literally, and warn the user. Selectors can be verified later with `/debug-test`.169170### TypeScript errors in generated files171172If `npx tsc --noEmit` reports errors in the generated spec or page object: read the errors, fix missing imports or type mismatches, and re-run. Common causes: missing page object import, wrong fixture type, incorrect `KubernetesClient` method signature.173174### Test passes but assertions are missing175176Compare assertion count with the original Cypress test. If the migrated test has fewer assertions, the agent may have silently dropped failing ones. Restore them using the intent documented in Phase 1.177178## Rules179180- Always read the Cypress source before writing any code181- Use Playwright MCP to verify selectors against the live UI182- Follow `.claude/e2e-context.md` for Playwright patterns and `.claude/migration-context.md` for Cypress translation tables183- **DO NOT commit** — the user handles git operations