# SkillPatch skill: test-framework-migration-skill

This skill migrates and converts test automation scripts between Selenium, Playwright, Puppeteer, and Cypress across multiple languages (Java, Python, JavaScript, TypeScript, C#). It provides a structured workflow for detecting source/target frameworks, handling language differences, and routing to appropriate reference documentation for accurate API mapping and pattern conversion. Ideal for QA teams modernizing or switching their end-to-end test infrastructure.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/test-framework-migration-skill
curl -sSL https://skillpatch.dev/install_skill/test-framework-migration-skill | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (13)

- `SKILL.md`
- `reference/cypress-to-playwright.md`
- `reference/cypress-to-selenium.md`
- `reference/overview.md`
- `reference/playbook.md`
- `reference/playwright-to-cypress.md`
- `reference/playwright-to-puppeteer.md`
- `reference/playwright-to-selenium.md`
- `reference/puppeteer-to-playwright.md`
- `reference/puppeteer-to-selenium.md`
- `reference/selenium-to-cypress.md`
- `reference/selenium-to-playwright.md`
- `reference/selenium-to-puppeteer.md`


### `SKILL.md`

```markdown
---
name: test-framework-migration-skill
description: >
  Migrates and converts test automation scripts between Selenium, Playwright,
  Puppeteer, and Cypress. Use when the user asks to migrate, convert, or port
  tests from one framework to another; rewrite tests in a different framework;
  or switch from Selenium to Playwright, Playwright to Selenium, Puppeteer to
  Playwright, Cypress to Playwright, or vice versa. Triggers on: "migrate",
  "convert", "port", "selenium to playwright", "playwright to selenium",
  "puppeteer to playwright", "cypress to playwright", "rewrite tests in",
  "switch from [framework] to [framework]".
languages:
  - Java
  - Python
  - JavaScript
  - TypeScript
  - C#
category: e2e-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

# Test Framework Migration Skill

You are a senior QA automation architect. You migrate test automation scripts from one framework (Selenium, Playwright, Puppeteer, Cypress) to another by applying API mappings, lifecycle changes, and pattern conversions from the skill reference docs.

## Step 1 — Detect Source Framework

Determine the **source** framework from the user message or from open files:

| Signal in message or code | Source framework |
|---------------------------|------------------|
| "Selenium", "WebDriver", "driver.findElement", "By.id", "ChromeDriver" | Selenium |
| "Playwright", "page.getByRole", "expect(locator).toBeVisible", "@playwright/test" | Playwright |
| "Puppeteer", "page.$", "page.goto", "puppeteer.launch" | Puppeteer |
| "Cypress", "cy.get", "cy.visit", "cy.contains", "cy.should" | Cypress |

If ambiguous (e.g. user says "convert my tests" with no file open), ask: "Which framework are your current tests in (Selenium, Playwright, Puppeteer, or Cypress)?"

## Step 2 — Detect Target Framework

Determine the **target** framework from the user message:

| User says... | Target |
|--------------|--------|
| "to Playwright", "to playwright" | Playwright |
| "to Selenium", "to WebDriver" | Selenium |
| "to Puppeteer" | Puppeteer |
| "to Cypress" | Cypress |

If the user only names the source (e.g. "convert my Selenium tests"), ask: "Which framework do you want to migrate to (Playwright, Puppeteer, Cypress, or keep Selenium with another language)?"

## Step 3 — Detect Language

| Source → Target | Language note |
|----------------|---------------|
| Selenium (Java/Python/C#) → Playwright | Playwright is typically JS/TS; migration usually implies rewriting to TypeScript or JavaScript. Mention this if source is Java/C#/Python. |
| Selenium (JS) → Playwright | Same language (JS/TS) possible. |
| Playwright/Puppeteer/Cypress → Selenium | Target can be Java, Python, JS, C#. Prefer same as project or ask. |
| Playwright ↔ Puppeteer ↔ Cypress | Typically stay in JS/TS. |

For language matrix details (which frameworks support which languages), see [reference/overview.md](reference/overview.md).

## Step 4 — Route to Reference

**Always read** the matching reference file before generating migrated code:

| Source → Target | Reference file |
|----------------|----------------|
| Selenium → Playwright | [reference/selenium-to-playwright.md](reference/selenium-to-playwright.md) |
| Playwright → Selenium | [reference/playwright-to-selenium.md](reference/playwright-to-selenium.md) |
| Selenium → Puppeteer | [reference/selenium-to-puppeteer.md](reference/selenium-to-puppeteer.md) |
| Puppeteer → Selenium | [reference/puppeteer-to-selenium.md](reference/puppeteer-to-selenium.md) |
| Puppeteer → Playwright | [reference/puppeteer-to-playwright.md](reference/puppeteer-to-playwright.md) |
| Playwright → Puppeteer | [reference/playwright-to-puppeteer.md](reference/playwright-to-puppeteer.md) |
| Cypress → Playwright | [reference/cypress-to-playwright.md](reference/cypress-to-playwright.md) |
| Playwright → Cypress | [reference/playwright-to-cypress.md](reference/playwright-to-cypress.md) |
| Selenium → Cypress | [reference/selenium-to-cypress.md](reference/selenium-to-cypress.md) |
| Cypress → Selenium | [reference/cypress-to-selenium.md](reference/cypress-to-selenium.md) |

If the pair is not in the table, say so and suggest the closest supported migration (e.g. add WebDriverIO later as a new reference file).

## Step 5 — Apply Mappings

Using the reference doc:

1. **Locators** — Convert using the API mapping table (e.g. `By.id("x")` → `page.getByRole(...)` or `page.locator('#x')`).
2. **Waits** — Convert wait strategy (explicit wait / auto-wait / cy.should).
3. **Actions** — Map click, type, select, etc.
4. **Assertions** — Map to target's assertion style.
5. **Lifecycle** — Adjust setup/teardown (driver vs page, launch vs connect).
6. **Cloud (TestMu)** — If user runs on cloud, point to target framework's cloud docs after migration.

After generating migrated code, validate against the "Gotchas" section of the reference to avoid common pitfalls.

## Cross-References for Deep Patterns

| Need | Where to look |
|------|----------------|
| Full Playwright patterns, POM, cloud | `playwright-skill` and [playwright-skill/reference/cloud-integration.md](../playwright-skill/reference/cloud-integration.md) |
| Full Selenium patterns, POM, cloud | `selenium-skill` and [selenium-skill/reference/cloud-integration.md](../selenium-skill/reference/cloud-integration.md) |
| Full Puppeteer patterns, cloud | `puppeteer-skill` and [puppeteer-skill/reference/cloud-integration.md](../puppeteer-skill/reference/cloud-integration.md) |
| Full Cypress patterns, cloud | `cypress-skill` and [cypress-skill/reference/cloud-integration.md](../cypress-skill/reference/cloud-integration.md) |
| TestMu capabilities (all frameworks) | [shared/testmu-cloud-reference.md](../shared/testmu-cloud-reference.md) |

## Validation Workflow

After generating migrated code:

1. Ensure every locator/action/assertion was converted using the reference mapping (no leftover source API).
2. Ensure lifecycle (setup/teardown) matches target framework.
3. If target is Playwright: use auto-wait assertions (`expect(locator).toBeVisible()`), not raw `waitForTimeout`.
4. If target is Cypress: no async/await with `cy` commands; use chain style.
5. If target is Selenium: use explicit `WebDriverWait`, never `Thread.sleep`.

## Reference Files Summary

| File | When to read |
|------|--------------|
| [reference/overview.md](reference/overview.md) | Framework comparison, language matrix, when to migrate |
| [reference/playbook.md](reference/playbook.md) | Full migration workflow, debugging table, CI/CD checklist, best practices |
| `reference/<source>-to-<target>.md` | Before converting any script for that pair |

```


### `reference/cypress-to-playwright.md`

````markdown
# Cypress → Playwright Migration

## API Mapping

### Locators

| Cypress | Playwright |
|---------|------------|
| `cy.get('[data-cy="x"]')` | `page.getByTestId('x')` (requires `data-testid="x"`) or `page.locator('[data-cy="x"]')` |
| `cy.get('[data-testid="btn"]')` | `page.getByTestId('btn')` |
| `cy.contains('Submit')` | `page.getByText('Submit')` or `page.getByRole('button', { name: 'Submit' })` |
| `cy.get('#id')` | `page.locator('#id')` |
| `cy.get('.class')` | `page.locator('.class')` |
| `cy.get('button').contains('Save')` | `page.locator('button').filter({ hasText: 'Save' })` or `page.getByRole('button', { name: 'Save' })` |

**Playwright:** Prefer getByRole, getByLabel, getByText, getByTestId. Cypress chains are synchronous-looking but async under the hood; Playwright is explicitly async/await.

### Waits / Assertions (Cypress retry-ability → Playwright auto-wait)

| Cypress | Playwright |
|---------|------------|
| `cy.get('.msg').should('be.visible')` | `await expect(page.locator('.msg')).toBeVisible()` |
| `cy.get('.msg').should('have.text', 'Saved')` | `await expect(page.locator('.msg')).toHaveText('Saved')` |
| `cy.url().should('include', '/dashboard')` | `await expect(page).toHaveURL(/\/dashboard/)` |
| `cy.get('h1').should('contain', 'Welcome')` | `await expect(page.locator('h1')).toContainText('Welcome')` |
| `cy.wait(3000)` | **Avoid.** Use `await expect(locator).toBeVisible()` or action that implies wait |
| Implicit retry on cy.get/cy.should | Playwright auto-waits on assertions and actions |

### Actions

| Cypress | Playwright |
|---------|------------|
| `cy.visit('/login')` | `await page.goto(baseURL + '/login')` or `await page.goto('/login')` with baseURL in config |
| `cy.get('#email').type('user@test.com')` | `await page.locator('#email').fill('user@test.com')` |
| `cy.get('button').click()` | `await page.getByRole('button').click()` or `await page.locator('button').click()` |
| `cy.get('#sel').select('value')` | `await page.locator('#sel').selectOption('value')` |
| `cy.intercept('GET', '/api/users').as('users')` then `cy.wait('@users')` | `await page.waitForResponse(resp => resp.url().includes('/api/users') && resp.request().method() === 'GET')` or `page.route()` for mocking |
| `cy.request('POST', '/api/login', body)` | `await request.post(baseURL + '/api/login', { data: body })` (request from @playwright/test) |

### Lifecycle

| Cypress | Playwright |
|---------|------------|
| `beforeEach(() => { cy.visit('/') })` | `test.beforeEach(async ({ page }) => { await page.goto('/'); });` or set baseURL in config and goto in test |
| No explicit "close"; Cypress manages browser | Test runner provides page; browser/context closed after test |
| `cy.clearCookies()`, `cy.clearLocalStorage()` | `await context.clearCookies()`; `await page.evaluate(() => localStorage.clear())` |
| `cy.fixture('data.json')` | `import data from './fixtures/data.json'` or `require` / fs read in beforeAll |

## Before / After Snippets

**Cypress (JavaScript):**
```javascript
cy.visit('/login');
cy.get('[data-cy="email"]').type('user@test.com');
cy.get('[data-cy="password"]').type('secret');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
cy.get('h1').should('contain', 'Dashboard');
```

**Playwright (TypeScript):**
```typescript
await page.goto('/login');
await page.getByTestId('email').fill('user@test.com');
await page.getByTestId('password').fill('secret');
await page.locator('button[type="submit"]').click();
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.locator('h1')).toContainText('Dashboard');
```

## Lifecycle / Setup

- **Cypress:** Cypress runner; `cy` commands are queued and run asynchronously but written without async/await. Config in `cypress.config.js`.
- **Playwright:** Async/await throughout. Config in `playwright.config.ts`; use `baseURL` for relative URLs. Tests use `test('...', async ({ page }) => { ... })`.

## Cloud (TestMu)

- Cypress on TestMu: LambdaTest Cypress CLI / plugin.
- Playwright on TestMu: CDP connection. See [playwright-skill/reference/cloud-integration.md](../../playwright-skill/reference/cloud-integration.md) and [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

## Gotchas

1. **Async/await required:** Cypress uses chain style (no await on cy); Playwright is async/await. Every action and assertion must be awaited.
2. **baseURL:** Cypress has baseURL in config; Playwright has the same. Use `page.goto('/path')` with config baseURL so relative paths work.
3. **No cy.wrap / aliases:** Cypress aliases (`.as()`) become variables or repeated locators in Playwright. Store locators in variables if reused.
4. **cy.intercept → page.route:** For mocking, use `await page.route('**/api/*', route => route.fulfill({ ... }))` or similar. For waiting on requests use `page.waitForResponse()`.
5. **Multiple tabs:** Cypress does not support multiple tabs well; Playwright does. If migrating tests that avoided multiple tabs, you can now use `context.waitForEvent('page')` or similar.

````


### `reference/cypress-to-selenium.md`

````markdown
# Cypress → Selenium Migration

## API Mapping

### Locators

| Cypress | Selenium (Java example) |
|---------|-------------------------|
| `cy.get('#x')` | `By.id("x")` or `By.cssSelector("#x")` |
| `cy.get('[data-cy="btn"]')` | `By.cssSelector("[data-cy='btn']")` |
| `cy.get('[data-testid="btn"]')` | `By.cssSelector("[data-testid='btn']")` |
| `cy.contains('Submit')` | `By.xpath("//*[contains(text(),'Submit')]")` or more specific |
| `cy.contains('button', 'Submit')` | `By.xpath("//button[contains(.,'Submit')]")` |
| `cy.get('.cls')` | `By.cssSelector(".cls")` |

**Selenium:** Use explicit wait before every find. Store `By` locators, not WebElement, to avoid stale references.

### Waits / Assertions

| Cypress | Selenium |
|---------|----------|
| `cy.get(sel).should('be.visible')` | `WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(sel)));` |
| `cy.get(sel).should('have.text', 'x')` | Wait for visible, then `Assertions.assertEquals("x", driver.findElement(By.cssSelector(sel)).getText());` |
| `cy.url().should('include', '/dashboard')` | `wait.until(ExpectedConditions.urlContains("/dashboard"));` |
| `cy.title().should('include', 'X')` | `wait.until(ExpectedConditions.titleContains("X"));` or getTitle() and assert |
| Cypress retry-ability | Selenium has no retry; always use WebDriverWait before get + action/assert. |

### Actions

| Cypress | Selenium |
|---------|----------|
| `cy.visit('/login')` | `driver.get(baseURL + "/login");` |
| `cy.get(sel).type('text')` | `WebElement el = wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(sel))); el.clear(); el.sendKeys("text");` |
| `cy.get(sel).click()` | `wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector(sel))).click();` |
| `cy.get('#sel').select('value')` | `new Select(driver.findElement(By.id("sel"))).selectByValue("value");` |
| `cy.intercept(...)` | No direct equivalent; use driver for network only if needed (e.g. DevTools). For mocking, consider test double at app/network layer. |
| `cy.request('POST', url, body)` | Use HttpClient/OkHttp in Java, or JS fetch in Node, outside Selenium. |

### Lifecycle

| Cypress | Selenium |
|---------|----------|
| `beforeEach(() => cy.visit('/'))` | `@BeforeEach` create driver and optionally `driver.get(baseURL);` |
| No explicit close | `@AfterEach` call `driver.quit();` |
| Viewport in config or `cy.viewport()` | `driver.manage().window().setSize(...)` or `maximize()` |

## Before / After Snippets

**Cypress (JavaScript):**
```javascript
cy.visit('/login');
cy.get('[data-cy="email"]').type('user@test.com');
cy.get('[data-cy="password"]').type('secret');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
```

**Selenium (Java):**
```java
driver.get(baseURL + "/login");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("[data-cy='email']"))).sendKeys("user@test.com");
driver.findElement(By.cssSelector("[data-cy='password']")).sendKeys("secret");
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("button[type='submit']"))).click();
wait.until(ExpectedConditions.urlContains("/dashboard"));
```

## Lifecycle / Setup

- **Cypress:** No driver; cy commands queued. Config in cypress.config.js.
- **Selenium:** Create WebDriver in setup, quit in teardown. Use WebDriverWait for every interaction that depends on DOM/visibility. Choose language (Java, Python, JS, C#) and test runner (JUnit, pytest, Mocha, NUnit).

## Cloud (TestMu)

- Cypress: LambdaTest Cypress CLI.
- Selenium: RemoteWebDriver + hub + DesiredCapabilities / LT:Options. See [selenium-skill/reference/cloud-integration.md](../../selenium-skill/reference/cloud-integration.md) and [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

## Gotchas

1. **Explicit waits required:** Cypress retries; Selenium does not. Add WebDriverWait + ExpectedConditions before every findElement + action or assertion.
2. **No cy.intercept in Selenium:** Network mocking is not built into Selenium. Use application-level mocks or test against a mocked backend.
3. **cy.request:** In Selenium you typically do API calls via the same language's HTTP client (e.g. Java HttpClient) if needed; not part of WebDriver.
4. **Language:** Cypress is JS/TS; Selenium supports Java, Python, C#, etc. Pick target language and convert syntax and lifecycle (e.g. JUnit @BeforeEach/@AfterEach).
5. **Stale elements:** Do not hold WebElement across navigations or DOM updates; re-find using By locators after wait.

````


### `reference/overview.md`

```markdown
# Migration Overview — Framework Comparison and Language Matrix

## Framework Comparison

| Aspect | Selenium | Playwright | Puppeteer | Cypress |
|--------|----------|------------|-----------|---------|
| **Protocol / model** | WebDriver (W3C), multi-vendor | CDP / WebDriver BiDi, modern | Chrome DevTools Protocol (CDP) | Proprietary runner + browser |
| **Languages** | Java, Python, JS, C#, Ruby, PHP | JS, TS, Python, Java, C# | JS, TS | JS, TS |
| **Waits** | Explicit (WebDriverWait) required | Auto-wait on actions/assertions | Manual (waitForSelector, etc.) | Built-in retry-ability on cy commands |
| **Execution** | In-process driver; can be remote (Grid) | In-process or connect to browser | In-process (Chrome/Chromium) | Runner + browser; no true multi-tab |
| **Cross-browser** | Chrome, Firefox, Safari, Edge via drivers | Chromium, Firefox, WebKit out of box | Chrome/Chromium (experimental others) | Chrome, Firefox, Edge, Safari (limited) |
| **Typical use** | Enterprise, multi-language teams, Grid | E2E tests, modern stack, API mock | Scraping, PDF, Chrome-only automation | E2E + component tests, dev-friendly |

## When to Migrate

| From → To | Common reasons |
|-----------|-----------------|
| Selenium → Playwright | Reduce flakiness (auto-wait), better debugging (trace), API mocking, single codebase (JS/TS). |
| Selenium → Puppeteer | Simplify stack to Chrome-only, lighter weight, scraping/PDF needs. |
| Playwright → Selenium | Need Java/Python/C# or existing Selenium Grid / enterprise tooling. |
| Puppeteer → Playwright | Need Firefox/WebKit, stronger test APIs (assertions, trace), multi-browser. |
| Cypress → Playwright | Need multiple tabs, non-Chromium browsers, or run in existing Node CI without Cypress runner. |
| Playwright → Cypress | Team preference for Cypress DX, component testing in Cypress, existing Cypress cloud. |

## Language Matrix

Use this to set expectations when the user does not specify a target language:

| Framework | Java | Python | JavaScript | TypeScript | C# | Ruby | PHP |
|-----------|------|--------|------------|------------|-----|------|-----|
| **Selenium** | Yes | Yes | Yes | — | Yes | Yes | Yes |
| **Playwright** | Yes | Yes | Yes | Yes | Yes | — | — |
| **Puppeteer** | — | — | Yes | Yes | — | — | — |
| **Cypress** | — | — | Yes | Yes | — | — | — |

- **Selenium (Java/Python/C#) → Playwright:** Playwright’s primary ecosystem is JS/TS. Migration usually means rewriting to TypeScript or JavaScript. Playwright does support Java, Python, and C#, but the most common migration path is to JS/TS.
- **Playwright/Puppeteer/Cypress → Selenium:** Target language can be any supported by Selenium; choose based on project or ask the user.
- **Puppeteer/Cypress:** No Java/Python/C#; migration to or from them implies JS/TS.

## Supported Migration Pairs

| Direction | Reference file |
|-----------|----------------|
| Selenium → Playwright | [selenium-to-playwright.md](selenium-to-playwright.md) |
| Playwright → Selenium | [playwright-to-selenium.md](playwright-to-selenium.md) |
| Selenium → Puppeteer | [selenium-to-puppeteer.md](selenium-to-puppeteer.md) |
| Puppeteer → Selenium | [puppeteer-to-selenium.md](puppeteer-to-selenium.md) |
| Puppeteer → Playwright | [puppeteer-to-playwright.md](puppeteer-to-playwright.md) |
| Playwright → Puppeteer | [playwright-to-puppeteer.md](playwright-to-puppeteer.md) |
| Cypress → Playwright | [cypress-to-playwright.md](cypress-to-playwright.md) |
| Playwright → Cypress | [playwright-to-cypress.md](playwright-to-cypress.md) |
| Selenium → Cypress | [selenium-to-cypress.md](selenium-to-cypress.md) |
| Cypress → Selenium | [cypress-to-selenium.md](cypress-to-selenium.md) |

Each reference file contains API mapping tables, before/after snippets, lifecycle notes, TestMu cloud pointers, and gotchas for that pair.

```


### `reference/playbook.md`

````markdown
# Test Framework Migration — Unified Playbook

## §1 — Migration Decision Framework

Use this table to decide whether to migrate between frameworks. Evaluate against your project's constraints before committing.

### Selenium to Playwright

| Migrate when | Stay when |
|-------------|-----------|
| Tests are flaky due to manual wait management | Large Java/Python/C# codebase with extensive Selenium Grid investment |
| Need built-in API mocking, tracing, or auto-wait | Team has deep Selenium expertise and stable suite |
| Want single JS/TS codebase with modern tooling | Regulatory or enterprise tooling requires Selenium |
| Need multi-browser (Chromium, Firefox, WebKit) without Grid | Already using RemoteWebDriver + cloud grid at scale |

### Selenium to Puppeteer

| Migrate when | Stay when |
|-------------|-----------|
| Chrome-only automation (scraping, PDF generation) | Need multi-browser support |
| Want lightweight Node.js library without test runner | Tests are in Java/Python/C# |
| Existing codebase is already Node.js | Need built-in assertions, tracing, or test runner |

### Selenium to Cypress

| Migrate when | Stay when |
|-------------|-----------|
| Team prefers Cypress DX and component testing | Need multi-tab or multi-window support |
| Want built-in retry-ability and time-travel debugging | Tests are in Java/Python/C# (Cypress is JS/TS only) |
| Primarily testing a single web application | Need cross-browser beyond Chrome/Firefox/Edge |

### Playwright to Selenium

| Migrate when | Stay when |
|-------------|-----------|
| Enterprise requires Selenium Grid or specific Selenium tooling | Playwright suite is stable and team is proficient |
| Need Ruby or PHP bindings not available in Playwright | Already using Playwright's tracing, auto-wait, and API mocking |
| Integrating with existing Java/C# test infrastructure | No strong reason to leave modern tooling |

### Puppeteer to Playwright

| Migrate when | Stay when |
|-------------|-----------|
| Need Firefox or WebKit support | Chrome-only scraping that works fine |
| Want built-in test runner, assertions, and tracing | Simple scripts with no test framework needs |
| Need auto-wait instead of manual `waitForSelector` | Minimal codebase not worth the effort |

### Cypress to Playwright

| Migrate when | Stay when |
|-------------|-----------|
| Need multiple tabs/windows, which Cypress does not support | Team is productive with Cypress DX |
| Need non-Chromium browsers (WebKit) | Cypress component testing is core to workflow |
| Want async/await control flow instead of chain-based | Invested in Cypress Cloud dashboard |

### Playwright to Cypress

| Migrate when | Stay when |
|-------------|-----------|
| Team prefers Cypress DX and time-travel debugging | Need multi-tab support |
| Want Cypress component testing features | Need WebKit testing |
| Existing Cypress Cloud investment | Already leveraging Playwright's tracing and API mocking |

For detailed API mappings for any pair, see the pair-specific reference files listed in [overview.md](overview.md).

## §2 — Language Matrix

| Framework | Java | Python | JavaScript | TypeScript | C# | Ruby | PHP |
|-----------|:----:|:------:|:----------:|:----------:|:---:|:----:|:---:|
| **Selenium** | Yes | Yes | Yes | -- | Yes | Yes | Yes |
| **Playwright** | Yes | Yes | Yes | Yes | Yes | -- | -- |
| **Puppeteer** | -- | -- | Yes | Yes | -- | -- | -- |
| **Cypress** | -- | -- | Yes | Yes | -- | -- | -- |

**Key implications for migration:**

- **Selenium (Java/Python/C#) to Playwright:** Most common path is to rewrite in TypeScript/JavaScript. Playwright also has Java, Python, and C# bindings if you prefer to keep the language.
- **Playwright/Puppeteer/Cypress to Selenium:** Target language can be any Selenium supports; choose based on team expertise.
- **Puppeteer/Cypress:** JS/TS only. Migration to or from these frameworks implies JS/TS.

## §3 — Migration Workflow (10-Step Process)

### Step 1: Audit existing test suite

```bash
# Count test files and lines
find tests/ -name "*.java" -o -name "*.py" -o -name "*.js" -o -name "*.ts" | wc -l
# Identify page objects
find . -path "*/pages/*" -o -path "*/pageobjects/*" | head -20
# Check for cloud/grid usage
grep -r "RemoteWebDriver\|cdp.lambdatest\|GRID_URL\|HUB_URL" --include="*.java" --include="*.js" --include="*.ts" --include="*.py" .
```

Document:
- Total test count and lines of code
- Number of page objects
- Frameworks and runners in use (JUnit, TestNG, pytest, Mocha, Jest)
- CI/CD pipeline configuration
- Cloud/grid dependencies (Selenium Grid, LambdaTest, BrowserStack)
- Custom utilities (retry logic, screenshot helpers, data providers)

### Step 2: Choose target language

Refer to the Language Matrix in [section 2](#2--language-matrix). If the source is Java Selenium and the target is Playwright, decide JS/TS vs Playwright Java bindings. For most Playwright migrations, TypeScript is recommended.

### Step 3: Set up target framework project

```bash
# Example: Playwright TypeScript
npm init -y
npm install -D @playwright/test
npx playwright install

# Example: Selenium Java (Maven)
mvn archetype:generate -DgroupId=com.tests -DartifactId=selenium-tests
# Add selenium-java, webdrivermanager to pom.xml

# Example: Cypress
npm init -y
npm install -D cypress
npx cypress open
```

Create the project structure for the target framework before migrating any tests.

### Step 4: Migrate infrastructure (config, base classes, utilities)

Migrate in this order:
1. Configuration files (base URL, timeouts, browser settings)
2. Base page class / shared utilities
3. Test fixtures and setup/teardown hooks
4. Custom wait helpers and retry logic
5. Reporting integration

### Step 5: Convert page objects

For each page object, convert locators, actions, and internal methods using the pair-specific API mapping tables in the reference files. See [overview.md](overview.md) for the list of all pair-specific references.

**Example — Selenium Java → Playwright TypeScript page object:**

```java
// BEFORE: Selenium Java
public class LoginPage {
    private By emailField = By.id("email");
    private By submitBtn = By.cssSelector("button[type='submit']");
    public LoginPage(WebDriver driver) { this.driver = driver; }
    public void login(String email, String pass) {
        wait.until(ExpectedConditions.visibilityOfElementLocated(emailField)).sendKeys(email);
        driver.findElement(submitBtn).click();
    }
}
```

```typescript
// AFTER: Playwright TypeScript
export class LoginPage {
    readonly emailField = this.page.getByLabel('Email');
    readonly submitBtn = this.page.getByRole('button', { name: 'Sign in' });
    constructor(private page: Page) {}
    async login(email: string, pass: string) {
        await this.emailField.fill(email);
        await this.submitBtn.click(); // auto-waits for visible + enabled
    }
}
```

**Locator conversion priority (Playwright target):**
1. Use `getByRole`, `getByLabel`, `getByText`, `getByTestId` where possible
2. Fall back to `page.locator(css)` for complex selectors
3. Avoid `xpath=` unless there is no CSS equivalent

### Step 6: Migrate tests in batches

Migrate tests in batches of 5-10, grouped by feature or page. After each batch:
- Run the batch locally
- Fix any conversion issues
- Commit the batch

Do NOT attempt to migrate all tests at once.

### Step 7: Run locally and fix issues

```bash
# Playwright
npx playwright test --headed

# Selenium (Maven)
mvn test -Dbrowser=chrome

# Cypress
npx cypress run

# Puppeteer (with Jest)
npx jest --runInBand
```

Use the debugging table in [section 6](#6--debugging-quick-reference) for common issues.

### Step 8: Update CI/CD pipeline

See the CI/CD Migration Checklist in [section 7](#7--cicd-migration-checklist).

### Step 9: Cloud migration

If running tests on a cloud grid (LambdaTest / TestMu):
- Update connection strings and capabilities for the target framework
- See pair-specific reference files for cloud (TestMu) sections
- See [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md) for cross-framework cloud configuration

### Step 10: Decommission old framework

1. Confirm all migrated tests pass in CI for at least 2 consecutive runs
2. Remove old framework dependencies from package.json / pom.xml / requirements.txt
3. Delete old test files, page objects, and utilities
4. Update documentation and README
5. Archive the old CI pipeline configuration

## §4 — Common Migration Patterns

For full API mapping tables, always consult the pair-specific reference file. Below are the most frequently needed cross-framework conversions.

### Locator Conversion

| Concept | Selenium | Playwright | Puppeteer | Cypress |
|---------|----------|------------|-----------|---------|
| By ID | `By.id("x")` | `page.locator('#x')` | `page.$('#x')` | `cy.get('#x')` |
| By CSS | `By.cssSelector(".c")` | `page.locator('.c')` | `page.$('.c')` | `cy.get('.c')` |
| By text | `By.xpath("//*[contains(text(),'T')]")` | `page.getByText('T')` | XPath or evaluate | `cy.contains('T')` |
| By role | No built-in | `page.getByRole('button', {name: 'X'})` | No built-in | No built-in |
| By test ID | `By.cssSelector("[data-testid='x']")` | `page.getByTestId('x')` | `page.$('[data-testid="x"]')` | `cy.get('[data-testid="x"]')` |

### Wait Strategy Conversion

| Concept | Selenium | Playwright | Puppeteer | Cypress |
|---------|----------|------------|-----------|---------|
| Wait for visible | `WebDriverWait` + `visibilityOfElementLocated` | Auto-wait on actions; `expect(loc).toBeVisible()` | `page.waitForSelector(sel, {visible:true})` | `cy.get(sel).should('be.visible')` |
| Wait for clickable | `elementToBeClickable` | Auto-wait on `.click()` | `waitForSelector` + `.click()` | `cy.get(sel).click()` (retries) |
| Wait for URL | `urlContains` | `expect(page).toHaveURL(...)` | Check `page.url()` in loop or `waitForNavigation` | `cy.url().should('include', ...)` |
| Hard sleep | `Thread.sleep(ms)` -- avoid | `page.waitForTimeout(ms)` -- avoid | `page.waitForTimeout(ms)` -- avoid | `cy.wait(ms)` -- avoid |

### Assertion Conversion

| Concept | Selenium | Playwright | Puppeteer | Cypress |
|---------|----------|------------|-----------|---------|
| Title contains | `assertTrue(getTitle().contains("X"))` | `expect(page).toHaveTitle(/X/)` | `expect(await page.title()).toContain('X')` | `cy.title().should('include', 'X')` |
| URL contains | `assertTrue(getCurrentUrl().contains("/d"))` | `expect(page).toHaveURL(/\/d/)` | `expect(page.url()).toContain('/d')` | `cy.url().should('include', '/d')` |
| Text equals | `assertEquals("t", el.getText())` | `expect(loc).toHaveText('t')` | `expect(await el.evaluate(e=>e.textContent)).toBe('t')` | `cy.get(s).should('have.text','t')` |
| Element visible | `assertTrue(el.isDisplayed())` | `expect(loc).toBeVisible()` | `expect(await el.isIntersectingViewport()).toBe(true)` | `cy.get(s).should('be.visible')` |

### Lifecycle Conversion

| Concept | Selenium | Playwright | Puppeteer | Cypress |
|---------|----------|------------|-----------|---------|
| Launch browser | `new ChromeDriver()` | `chromium.launch()` or test fixture | `puppeteer.launch()` | Managed by runner |
| Navigate | `driver.get(url)` | `page.goto(url)` | `page.goto(url)` | `cy.visit(url)` |
| Close | `driver.quit()` | `browser.close()` or fixture teardown | `browser.close()` | Managed by runner |
| Viewport | `window().maximize()` | `setViewportSize({w,h})` | `page.setViewport({w,h})` | `cy.viewport(w,h)` |

## §5 — Advanced Pattern Conversions

### Alert / Dialog Handling

| Framework | Pattern |
|-----------|---------|
| Selenium | `wait.until(alertIsPresent()); driver.switchTo().alert().accept();` |
| Playwright | `page.on('dialog', d => d.accept());` -- register before triggering action |
| Puppeteer | `page.on('dialog', d => d.accept());` -- same pattern as Playwright |
| Cypress | `cy.on('window:alert', () => true);` for alerts; `cy.on('window:confirm', () => true);` for confirms -- stub before action |

### iframe Handling

| Framework | Pattern |
|-----------|---------|
| Selenium | `driver.switchTo().frame("name"); ... driver.switchTo().defaultContent();` |
| Playwright | `page.frameLocator('iframe[name="name"]').locator('button').click();` |
| Puppeteer | `const frame = page.frames().find(f => f.name() === 'name'); await frame.click('button');` |
| Cypress | `cy.get('iframe').its('0.contentDocument.body').then(cy.wrap).find('button').click();` |

### Multiple Tabs / Windows

| Framework | Pattern |
|-----------|---------|
| Selenium | `driver.switchTo().newWindow(WindowType.TAB);` or iterate `getWindowHandles()` |
| Playwright | `const [newPage] = await Promise.all([context.waitForEvent('page'), page.click('a')]);` |
| Puppeteer | `const [newPage] = await Promise.all([browser.waitForTarget(t => t.opener()), page.click('a')]);` then `await newPage.page()` |
| Cypress | Not supported natively; workaround by removing `target=_blank` or using `cy.origin()` |

### Network Mocking / Interception

| Framework | Pattern |
|-----------|---------|
| Selenium | No built-in; use DevTools protocol or application-level mocks |
| Playwright | `await page.route('**/api/*', route => route.fulfill({body: '...'}));` |
| Puppeteer | `await page.setRequestInterception(true); page.on('request', req => {...});` |
| Cypress | `cy.intercept('GET', '/api/*', {body: ...}).as('alias');` |

### File Upload

| Framework | Pattern |
|-----------|---------|
| Selenium | `driver.findElement(By.id("upload")).sendKeys("/path/to/file");` |
| Playwright | `await page.locator('#upload').setInputFiles('/path/to/file');` |
| Puppeteer | `const input = await page.$('#upload'); await input.uploadFile('/path/to/file');` |
| Cypress | `cy.get('#upload').selectFile('path/to/file');` |

### Attribute Reading

| Framework | Pattern |
|-----------|---------|
| Selenium | `element.getAttribute("href")` |
| Playwright | `await locator.getAttribute('href')` or `expect(loc).toHaveAttribute('href', /pattern/)` |
| Puppeteer | `await element.evaluate(el => el.getAttribute('href'))` |
| Cypress | `cy.get(sel).should('have.attr', 'href', '/expected')` or `cy.get(sel).invoke('attr', 'href')` |

### JavaScript Execution

| Framework | Pattern |
|-----------|---------|
| Selenium | `((JavascriptExecutor) driver).executeScript("return arguments[0].scrollHeight", element);` |
| Playwright | `await locator.evaluate(el => el.scrollHeight)` or `await page.evaluate(() => document.title)` |
| Puppeteer | `await element.evaluate(el => el.scrollHeight)` or `await page.evaluate(() => document.title)` |
| Cypress | `cy.get(sel).then($el => $el[0].scrollHeight)` or `cy.window().then(win => win.document.title)` |

## §6 — Debugging Quick-Reference

| # | Problem | Likely Cause | Fix |
|---|---------|-------------|-----|
| 1 | Tests pass locally, fail in CI | Display, timing, or viewport differences | Run headless locally; set explicit viewport (e.g. `--window-size=1920,1080`); increase timeouts; check for missing fonts/deps in CI image |
| 2 | Element not found | Element not yet in DOM, wrong locator, or inside iframe | Use explicit wait or auto-wait assertion; verify locator in browser DevTools; check if element is inside an iframe or shadow DOM |
| 3 | Stale element reference | DOM re-rendered after element was located | Re-locate element after each navigation or DOM change; in Selenium use `By` locators with `WebDriverWait`; in Playwright use `Locator` (auto-resolves) |
| 4 | Timeout waiting for element | Element never appears, wrong page, or selector mismatch | Verify URL is correct; check for redirects; validate selector; increase timeout; add debug screenshot before wait |
| 5 | Alert/dialog not handled | Alert appeared before handler registered, or handler registered too late | Selenium: wait for `alertIsPresent()` before `switchTo().alert()`; Playwright/Puppeteer: register `page.on('dialog')` before the action that triggers it; Cypress: use `cy.on('window:confirm')` before action |
| 6 | Multiple tab/window issues | Wrong browsing context after opening new tab | Selenium: iterate `getWindowHandles()` and `switchTo().window(handle)`; Playwright: use `context.waitForEvent('page')`; Cypress: remove `target=_blank` or restructure test |
| 7 | File upload fails | Input element hidden or not a file input | Selenium: `sendKeys()` only works on `<input type="file">`; Playwright: use `setInputFiles()`; ensure the element is the actual file input, not a styled wrapper |
| 8 | iframe elements not found | Test is in the wrong browsing context | Selenium: `switchTo().frame()` before interacting, `switchTo().defaultContent()` after; Playwright: use `frameLocator()`; Puppeteer: get frame from `page.frames()` |
| 9 | Network mock not intercepting | Route pattern does not match actual request URL | Log actual request URLs; verify glob/regex pattern; ensure mock is registered before navigation; check request method (GET vs POST) |
| 10 | Parallel execution failures | Shared state between tests (cookies, DB, global variables) | Isolate browser contexts per test; use unique test data; avoid shared mutable state; in Selenium use `ThreadLocal<WebDriver>` |
| 11 | Screenshots blank or wrong size | Headless viewport not set, or screenshot taken after browser closed | Set explicit viewport size; take screenshot before teardown; verify browser is still open when screenshot is captured |
| 12 | Cloud grid connection fails | Wrong hub URL, expired credentials, or capability mismatch | Verify hub URL and credentials; check `LT:Options` / capabilities format; ensure browser version is available on grid; see [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md) |
| 13 | async/await errors (Playwright/Puppeteer) | Missing `await` on async call, or using `await` with Cypress `cy` commands | Playwright/Puppeteer: ensure every action and assertion is `await`ed; Cypress: never use `async/await` with `cy` commands |
| 14 | Compile/type errors after migration | Incorrect API usage for target framework, leftover source framework calls | Review the pair-specific API mapping table; search for leftover imports (`import org.openqa.selenium`, `require('puppeteer')`, etc.); run TypeScript compiler or linter |
| 15 | Select/dropdown not working | Different API between frameworks for `<select>` elements | Selenium: `new Select(el).selectByValue()`; Playwright: `locator.selectOption()`; Puppeteer: `page.select()`; Cypress: `cy.get(sel).select()` |
| 16 | Hover/drag-and-drop not working | Actions API differs between frameworks | Selenium: `new Actions(driver).moveToElement(el).perform()`; Playwright: `locator.hover()` or `page.dragAndDrop()`; Puppeteer: `page.hover()` or mouse API; Cypress: `cy.get(el).trigger('mouseover')` or plugins |

## §7 — CI/CD Migration Checklist

Follow these steps when migrating CI/CD pipelines to a new test framework:

| # | Step | Details |
|---|------|---------|
| 1 | Install target framework in CI | Add install steps (e.g. `npm ci && npx playwright install --with-deps` for Playwright; add Maven dependencies for Selenium) |
| 2 | Update environment variables | Migrate `SELENIUM_REMOTE_URL`, `GRID_URL` to target equivalents; update `LT_USERNAME` / `LT_ACCESS_KEY` references if connection method changes |
| 3 | Update test run command | Replace `mvn test` with `npx playwright test`, or `npx cypress run`, etc. |
| 4 | Configure headless mode | Selenium: `--headless=new --no-sandbox --disable-dev-shm-usage`; Playwright: headless by default; Cypress: `npx cypress run` is headless; Puppeteer: `launch({headless: true})` |
| 5 | Set up artifact collection | Upload test results, screenshots, videos, and traces as CI artifacts; adjust paths for target framework output directories |
| 6 | Configure parallel execution | Selenium: TestNG parallel or JUnit parallel; Playwright: `--workers=N` or `fullyParallel: true` in config; Cypress: `cypres
...<truncated>
````


### `reference/playwright-to-cypress.md`

````markdown
# Playwright → Cypress Migration

## API Mapping

### Locators

| Playwright | Cypress |
|------------|---------|
| `page.locator('#x')` | `cy.get('#x')` |
| `page.getByRole('button', { name: 'Submit' })` | `cy.contains('button', 'Submit')` or `cy.get('button').contains('Submit')` |
| `page.getByLabel('Email')` | `cy.get('label').contains('Email').parent().find('input')` or `cy.get('input[name=email]')` / data-cy |
| `page.getByTestId('btn')` | `cy.get('[data-testid="btn"]')` or `cy.get('[data-cy="btn"]')` (Cypress recommends data-cy) |
| `page.getByText('Welcome')` | `cy.contains('Welcome')` |
| `page.locator('.item').all()` | `cy.get('.item')` (returns chainable; use .each or assertions) |

**Cypress:** Prefer `data-cy` or `data-testid` selectors. No getByRole; use `cy.contains()` or `cy.get(selector)`.

### Waits / Assertions

| Playwright | Cypress |
|------------|---------|
| `await expect(locator).toBeVisible()` | `cy.get(selector).should('be.visible')` |
| `await expect(locator).toHaveText('x')` | `cy.get(selector).should('have.text', 'x')` |
| `await expect(page).toHaveURL(/\/dashboard/)` | `cy.url().should('include', '/dashboard')` |
| `await expect(page).toHaveTitle(/X/)` | `cy.title().should('include', 'X')` |
| Auto-wait on actions | Cypress has built-in retry-ability on cy.get and .should; no explicit wait needed for visibility |
| `page.waitForTimeout(ms)` | `cy.wait(ms)` (avoid; use .should instead) |

### Actions

| Playwright | Cypress |
|------------|---------|
| `await page.goto('/login')` | `cy.visit('/login')` (uses baseURL if set in config) |
| `await locator.fill('text')` | `cy.get(selector).type('text')` (or .clear() then .type) |
| `await locator.click()` | `cy.get(selector).click()` |
| `await page.locator('#sel').selectOption('v')` | `cy.get('#sel').select('v')` |
| `page.on('dialog', d => d.accept())` | `cy.on('window:alert', (text) => { ... })` or stub; for confirm use stub |
| `page.route(...)` for mocking | `cy.intercept('GET', '/api/**', { fixture: 'data.json' })` |
| `await request.post(...)` (APIRequestContext) | `cy.request('POST', '/api/...', body)` |

### Lifecycle

| Playwright | Cypress |
|------------|---------|
| `test.beforeEach(async ({ page }) => { await page.goto('/'); });` | `beforeEach(() => { cy.visit('/'); });` |
| Fixture-provided `page` | No page object; use `cy` in each test |
| `await browser.close()` | Cypress runner manages browser; no explicit close in test |
| `context.storageState()` for auth | `cy.session()` (Cypress 8+) or preserve cookies/localStorage |

## Before / After Snippets

**Playwright (TypeScript):**
```typescript
await page.goto('/login');
await page.getByLabel('Email').fill('user@test.com');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/dashboard/);
```

**Cypress (JavaScript):**
```javascript
cy.visit('/login');
cy.get('input[type="email"]').type('user@test.com');
cy.get('input[type="password"]').type('secret');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
```

## Lifecycle / Setup

- **Playwright:** Async/await; config in playwright.config.ts; fixtures provide page.
- **Cypress:** No async/await with cy commands; config in cypress.config.js; use beforeEach for setup. Do not assign cy.get() to a variable for later use (commands are queued).

## Cloud (TestMu)

- Playwright: CDP to LambdaTest.
- Cypress: LambdaTest Cypress CLI. See [cypress-skill/reference/cloud-integration.md](../../cypress-skill/reference/cloud-integration.md) and [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

## Gotchas

1. **No async/await with cy:** Never use async/await with Cypress commands. Write synchronous-looking code; Cypress queues commands.
2. **Do not store cy.get() for later:** Commands run asynchronously; use the chain or re-call cy.get() in the same flow. Avoid `const btn = cy.get('button');` then later `btn.click()`.
3. **getByRole/getByLabel:** Cypress has no equivalent; use cy.get(selector) or cy.contains(). Prefer data-cy or data-testid for stability.
4. **Multiple tabs:** Cypress does not support multiple browser tabs. If the Playwright test uses multiple pages/tabs, simplify to single tab or skip that part and document the limitation.
5. **Request mocking:** Playwright uses page.route(); Cypress uses cy.intercept(). Map intercept patterns and fixtures accordingly.

````


### `reference/playwright-to-puppeteer.md`

````markdown
# Playwright → Puppeteer Migration

## API Mapping

### Locators

| Playwright | Puppeteer |
|------------|-----------|
| `page.locator('#x')` | `await page.$('#x')` (returns ElementHandle; use immediately for action or store for one-off use) |
| `page.getByRole('button', { name: 'Submit' })` | No direct equivalent; use `page.$('button')` or CSS/XPath that matches text (e.g. `page.$('xpath///button[contains(.,"Submit")]')`) |
| `page.getByLabel('Email')` | `page.$('input[type=email]')` or `page.$('[aria-label="Email"]')` or form-associated selector |
| `page.getByTestId('btn')` | `page.$('[data-testid="btn"]')` |
| `page.locator('.item').all()` | `await page.$$('.item')` |

**Puppeteer:** No role/label locators; use CSS, XPath, or data attributes. Prefer stable selectors (id, data-testid).

### Waits

| Playwright | Puppeteer |
|------------|-----------|
| Auto-wait on `locator.click()` | No auto-wait; use `await page.waitForSelector('#btn', { visible: true }); await page.click('#btn');` |
| `await expect(locator).toBeVisible()` | `await page.waitForSelector(selector, { visible: true, timeout: 10000 });` |
| `await page.goto(url)` (waits for load) | `await page.goto(url, { waitUntil: 'networkidle0' });` or `domcontentloaded` |
| No waitForTimeout | `await new Promise(r => setTimeout(r, ms))` only for debugging; prefer waitForSelector/waitForFunction |

### Actions

| Playwright | Puppeteer |
|------------|-----------|
| `await locator.fill('text')` | `await page.$('#x')` then `(await page.$('#x')).type('text')` or `page.evaluate((sel, t) => { document.querySelector(sel).value = t; }, '#x', 'text');` |
| `await locator.click()` | `await page.click('#selector')` (after waitForSelector if needed) |
| `await page.locator('#sel').selectOption('v')` | `await page.select('#sel', 'v');` |
| `page.on('dialog', d => d.accept())` | Same in Puppeteer |
| `page.frameLocator('iframe')` | `const frame = page.frames().find(f => f.name() === 'x');` then `frame.$()`, `frame.click()`, etc. |
| New tab | `context.waitForEvent('page')` in Playwright | `const [newPage] = await Promise.all([new Promise(r => page.once('popup', r)), page.click('a[target=_blank]')]);` |

### Assertions

| Playwright | Puppeteer |
|------------|-----------|
| `await expect(page).toHaveTitle(/X/)` | `const title = await page.title(); expect(title).toMatch(/X/);` (Jest/Mocha) |
| `await expect(page).toHaveURL(/\/dashboard/)` | `expect(page.url()).toMatch(/\/dashboard/);` |
| `await expect(locator).toHaveText('x')` | `const text = await (await page.$('#el')).evaluate(el => el.textContent); expect(text).toContain('x');` |

### Lifecycle

| Playwright | Puppeteer |
|------------|-----------|
| Test fixture `{ page }` or `chromium.launch()` + `newContext()` + `newPage()` | `puppeteer.launch()` then `browser.newPage()` |
| `await page.goto(url)` | `await page.goto(url, { waitUntil: 'networkidle0' })` |
| `await browser.close()` | `await browser.close()` |
| `context.setViewportSize(...)` | `await page.setViewport({ width, height })` |

## Before / After Snippets

**Playwright (TypeScript):**
```typescript
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill('user@test.com');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/dashboard/);
```

**Puppeteer (JavaScript):**
```javascript
await page.goto('https://example.com/login', { waitUntil: 'networkidle0' });
await page.type('input[type="email"]', 'user@test.com');
await page.waitForSelector('button[type="submit"]', { visible: true });
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle0' });
expect(page.url()).toMatch(/\/dashboard/);
```

## Lifecycle / Setup

- **Playwright:** Often uses @playwright/test with config and fixtures.
- **Puppeteer:** Typically manual launch/newPage/close; use Mocha/Jest or similar for structure. No built-in multi-browser config like Playwright projects.

## Cloud (TestMu)

- Playwright: CDP to LambdaTest (see playwright-skill/reference/cloud-integration.md).
- Puppeteer: WebSocket connect to LambdaTest (see puppeteer-skill/reference/cloud-integration.md) and [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

## Gotchas

1. **Add explicit waits:** Playwright auto-waits; Puppeteer does not. After migration, add `waitForSelector` or `waitForNavigation` before clicks/navigation-dependent asserts.
2. **getByRole/getByLabel:** Replace with CSS/XPath or data-testid; document the semantic meaning so selectors stay maintainable.
3. **fill vs type:** Playwright `fill()` clears and sets value; in Puppeteer use `element.type()` or `evaluate` to set value. For keyboard simulation use `page.keyboard.type()`.
4. **Chrome-only:** Playwright can run Firefox/WebKit; moving to Puppeteer limits you to Chrome/Chromium.
5. **Stale handles:** In Puppeteer, ElementHandles can go stale after navigation or DOM update; re-query with `page.$()` when in doubt.

````


### `reference/playwright-to-selenium.md`

````markdown
# Playwright → Selenium Migration

## API Mapping

### Locators

| Playwright (JS/TS) | Selenium (Java example; JS similar) |
|--------------------|--------------------------------------|
| `page.getByRole('button', { name: 'Submit' })` | `By.xpath("//button[normalize-space()='Submit']")` or `By.cssSelector("button")` + match text; prefer `By.id` if known |
| `page.getByLabel('Email')` | `By.xpath("//label[contains(.,'Email')]/following::input[1]")` or `By.cssSelector("input[name=email]")` |
| `page.getByPlaceholder('Enter email')` | `By.cssSelector("input[placeholder='Enter email']")` |
| `page.getByText('Welcome')` | `By.xpath("//*[contains(text(),'Welcome')]")` |
| `page.getByTestId('submit-btn')` | `By.cssSelector("[data-testid='submit-btn']")` |
| `page.locator('#x')` | `By.id("x")` or `By.cssSelector("#x")` |
| `page.locator('.cls')` | `By.cssSelector(".cls")` |

**Selenium:** No built-in role/label locators; use CSS, ID, or XPath. Prefer `By.id`, `By.cssSelector`, then XPath.

### Waits

| Playwright | Selenium |
|------------|----------|
| `await expect(locator).toBeVisible()` | `WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("x")));` |
| `await locator.click()` (auto-waits) | Wait for clickable then click: `wait.until(ExpectedConditions.elementToBeClickable(By.id("x"))).click();` |
| `await page.goto(url)` (auto-waits load) | `driver.get(url);` then optionally wait for specific element |
| `page.waitForTimeout(ms)` | **Avoid in both.** In Selenium use explicit `WebDriverWait`, never `Thread.sleep` in production tests. |

### Actions

| Playwright | Selenium |
|------------|----------|
| `await locator.fill('text')` | `WebElement el = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("x"))); el.clear(); el.sendKeys("text");` |
| `await locator.click()` | `wait.until(ExpectedConditions.elementToBeClickable(By.id("x"))).click();` |
| `await page.locator('#sel').selectOption('v')` | `new Select(driver.findElement(By.id("sel"))).selectByValue("v");` |
| `page.on('dialog', d => d.accept())` | `driver.switchTo().alert().accept();` (after alert is present) |
| `page.frameLocator('iframe[name="name"]')` | `driver.switchTo().frame("name");` then `driver.findElement(...)` |
| New tab: `context.waitForEvent('page')` | `driver.switchTo().newWindow(WindowType.TAB);` or switch by handle |

### Assertions

| Playwright | Selenium |
|------------|----------|
| `await expect(page).toHaveTitle(/X/)` | `Assertions.assertTrue(driver.getTitle().contains("X"));` |
| `await expect(page).toHaveURL(/\/dashboard/)` | `Assertions.assertTrue(driver.getCurrentUrl().contains("/dashboard"));` |
| `await expect(locator).toHaveText('text')` | `Assertions.assertEquals("text", driver.findElement(By.id("x")).getText());` (after wait for visible) |
| `await expect(locator).toBeVisible()` | `Assertions.assertTrue(wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("x"))).isDisplayed());` |

### Lifecycle

| Playwright | Selenium |
|------------|----------|
| `const browser = await chromium.launch();` etc. | `WebDriver driver = new ChromeDriver();` |
| `await page.goto(url)` | `driver.get(url);` |
| `await browser.close()` | `driver.quit();` (in @AfterEach / teardown) |
| `context.setViewportSize({ width, height })` | `driver.manage().window().maximize();` or `driver.manage().window().setSize(new Dimension(w, h));` |

## Before / After Snippets

**Playwright (TypeScript):**
```typescript
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill('user@test.com');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/dashboard/);
```

**Selenium (Java):**
```java
driver.get("https://example.com/login");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("input[type='email']"))).sendKeys("user@test.com");
driver.findElement(By.cssSelector("input[type='password']")).sendKeys("secret");
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("button[type='submit']"))).click();
wait.until(ExpectedConditions.urlContains("/dashboard"));
```

## Lifecycle / Setup

- **Playwright:** Test runner provides `page` via fixture; config in `playwright.config.ts`.
- **Selenium:** In JUnit/TestNG, create `WebDriver` in `@BeforeEach`, call `driver.quit()` in `@AfterEach`. Use explicit `WebDriverWait` for every interaction that depends on element visibility/clickability; do not rely on implicit wait or Thread.sleep.

## Cloud (TestMu)

- Playwright on TestMu: CDP over `wss://cdp.lambdatest.com/playwright?capabilities=...`.
- Selenium on TestMu: `RemoteWebDriver` with hub URL and `DesiredCapabilities` / `LT:Options`.  
  See [selenium-skill/reference/cloud-integration.md](../../selenium-skill/reference/cloud-integration.md) and [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

## Gotchas

1. **Explicit waits required:** Playwright auto-waits; Selenium does not. Every find + action should be guarded by `WebDriverWait` + `ExpectedConditions` (visibility, clickability, etc.).
2. **No getByRole/getByLabel in Selenium:** Replace with CSS, ID, or XPath. Prefer stable selectors (id, data-testid, name).
3. **Stale element references:** In Selenium, store `By` locators and re-find after waits; avoid holding `WebElement` across navigations or DOM updates.
4. **Dialogs:** In Playwright you listen for `dialog` before the action; in Selenium you switch to alert after it appears. Ensure wait for alert before `switchTo().alert()`.
5. **Language:** Playwright tests are often TypeScript/JS. Migrating to Selenium allows Java, Python, C#; choose based on project and convert syntax accordingly.

````


### `reference/puppeteer-to-playwright.md`

````markdown
# Puppeteer → Playwright Migration

## API Mapping

### Locators

| Puppeteer | Playwright |
|-----------|------------|
| `await page.$('#x')` | `page.locator('#x')` (Locator; use with await on actions) |
| `await page.$$('.item')` | `page.locator('.item')` then use `.all()` or iterate; or `page.locator('.item').count()` |
| `page.$('xpath///button')` | `page.locator('xpath=//button')` |
| ElementHandle from `page.$()` | Prefer keeping Locator: `page.locator('#x')`; Playwright re-resolves on each action |

**Playwright preference:** Use `page.getByRole()`, `getByLabel()`, `getByPlaceholder()`, `getByText()`, `getByTestId()` where possible instead of raw selectors.

### Waits

| Puppeteer | Playwright |
|-----------|------------|
| `await page.waitForSelector('.sel', { visible: true })` | Not needed; `await page.locator('.sel').click()` auto-waits. Or `await expect(page.locator('.sel')).toBeVisible()` |
| `await page.waitForNavigation({ waitUntil: 'networkidle0' })` | Usually unnecessary; actions wait for load. For explicit: `await page.waitForURL(/\/dashboard/)` or wait for element |
| `await page.waitForFunction(...)` | `await page.locator(...).evaluate(...)` or `await expect(locator).toHaveText(...)` |
| `page.waitForTimeout(ms)` | **Avoid.** Use `expect(locator).toBeVisible()` or action that implies wait |

### Actions

| Puppeteer | Playwright |
|-----------|------------|
| `await page.type('#x', 'text')` | `await page.locator('#x').fill('text')` or `await page.locator('#x').pressSequentially('text')` |
| `await page.click('#btn')` | `await page.locator('#btn').click()` |
| `await page.select('#sel', 'value')` | `await page.locator('#sel').selectOption('value')` |
| `page.on('dialog', d => d.accept())` | Same: `page.on('dialog', d => d.accept());` |
| Frame: `page.frames()` / `frame.$()` | `const frame = page.frameLocator('iframe[name="x"]'); frame.locator('button').click();` |
| `await page.setViewport({ width, height })` | `await page.setViewportSize({ width, height })` or set in context |

### Assertions

| Puppeteer | Playwright |
|-----------|------------|
| `const title = await page.title(); expect(title).toContain('X')` | `await expect(page).toHaveTitle(/X/)` |
| `expect(page.url()).toContain('/dashboard')` | `await expect(page).toHaveURL(/\/dashboard/)` |
| `await (await page.$('#x')).evaluate(el => el.textContent)` | `await expect(page.locator('#x')).toHaveText('...')` or `await page.locator('#x').textContent()` |

### Lifecycle

| Puppeteer | Playwright |
|-----------|------------|
| `const browser = await puppeteer.launch(); const page = await browser.newPage();` | Use test fixture: `test('...', async ({ page }) => { ... })` or `const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage();` |
| `await page.goto(url)` | `await page.goto(url)` (same) |
| `await browser.close()` | `await browser.close()` or test runner teardown |
| `page.setViewport(...)` | `context.setViewportSize(...)` or in config |

## Before / After Snippets

**Puppeteer (JavaScript):**
```javascript
await page.goto('https://example.com/login', { waitUntil: 'networkidle0' });
await page.type('#username', 'user@test.com');
await page.type('#password', 'secret');
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle0' });
```

**Playwright (TypeScript):**
```typescript
await page.goto('https://example.com/login');
await page.locator('#username').fill('user@test.com');
await page.locator('#password').fill('secret');
await page.locator('button[type="submit"]').click();
await expect(page).toHaveURL(/\/dashboard/);
```

## Lifecycle / Setup

- **Puppeteer:** Manual launch, newPage(), close. Often no test runner or Mocha/Jest.
- **Playwright:** Use `@playwright/test` and `defineConfig`; `page` and `context` provided by fixture. Optional `playwright.config.ts` with projects for multiple browsers (Chromium, Firefox, WebKit).

## Cloud (TestMu)

- Puppeteer: connect via LambdaTest WebSocket.
- Playwright: CDP to `wss://cdp.lambdatest.com/playwright?capabilities=...`. See [playwright-skill/reference/cloud-integration.md](../../playwright-skill/reference/cloud-integration.md) and [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

## Gotchas

1. **Auto-wait:** Playwright auto-waits on actions and assertions; remove explicit `waitForSelector`/`waitForNavigation` where they only wait for visibility/navigation.
2. **fill vs type:** Playwright's `fill()` clears and types; use `pressSequentially()` for key-by-key simulation if needed.
3. **Locator vs handle:** Don't store ElementHandle-like references; keep Locators and reuse them (Playwright re-resolves).
4. **Multi-browser:** Playwright supports Chromium, Firefox, WebKit out of the box; Puppeteer is Chrome/Chromium. After migration you can add Firefox/WebKit projects.
5. **Test runner:** Playwright has built-in test runner with fixtures, config, and reporting; consider moving to `npx playwright test` and `playwright.config.ts`.

````


### `reference/puppeteer-to-selenium.md`

````markdown
# Puppeteer → Selenium Migration

## API Mapping

### Locators

| Puppeteer | Selenium (Java/JS) |
|-----------|--------------------|
| `await page.$('#x')` | `driver.findElement(By.id("x"))` or `By.cssSelector("#x")` |
| `await page.$$('.item')` | `driver.findElements(By.cssSelector(".item"))` |
| `page.$('xpath///button')` | `By.xpath("//button")` |
| `page.$('.cls')` | `By.cssSelector(".cls")` |

**Selenium:** No ElementHandle; you get WebElement. Use explicit wait before find: `wait.until(ExpectedConditions.presenceOfElementLocated(By.id("x")));`

### Waits

| Puppeteer | Selenium |
|-----------|----------|
| `await page.waitForSelector('.sel', { visible: true })` | `WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".sel")));` |
| `await page.waitForNavigation({ waitUntil: 'networkidle0' })` | After click: wait for URL or specific element (e.g. `ExpectedConditions.urlContains("/dashboard")`) |
| `await page.waitForFunction('document.readyState === "complete"')` | Usually implicit after `driver.get()`; for SPA use wait for element |
| `await page.waitForResponse(resp => resp.url().includes('/api'))` | No direct equivalent; use explicit wait for element that appears after API call |

### Actions

| Puppeteer | Selenium |
|-----------|----------|
| `await page.type('#x', 'text')` | `WebElement el = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("x"))); el.clear(); el.sendKeys("text");` |
| `await page.click('#btn')` | `wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#btn"))).click();` |
| `await page.select('#sel', 'value')` | `new Select(driver.findElement(By.id("sel"))).selectByValue("value");` |
| `page.on('dialog', d => d.accept())` | After action that opens alert: `driver.switchTo().alert().accept();` (with wait if needed) |
| Frame: `page.frames()` / `frame.$()` | `driver.switchTo().frame("frameName");` then find elements on driver |
| `await page.setViewport({ width, height })` | `driver.manage().window().setSize(new Dimension(width, height));` |

### Assertions

| Puppeteer | Selenium |
|-----------|----------|
| `const title = await page.title(); expect(title).toContain('X')` | `Assertions.assertTrue(driver.getTitle().contains("X"));` |
| `page.url()` | `driver.getCurrentUrl()` |
| `await (await page.$('#x')).evaluate(el => el.textContent)` | `wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("x"))).getText();` |

### Lifecycle

| Puppeteer | Selenium |
|-----------|----------|
| `const browser = await puppeteer.launch(); const page = await browser.newPage();` | `WebDriver driver = new ChromeDriver();` |
| `await page.goto(url)` | `driver.get(url);` |
| `await browser.close()` | `driver.quit();` |
| `await page.setViewport({ width, height })` | `driver.manage().window().setSize(...)` or `maximize()` |

## Before / After Snippets

**Puppeteer (JavaScript):**
```javascript
await page.goto('https://example.com/login', { waitUntil: 'networkidle0' });
await page.type('#username', 'user@test.com');
await page.type('#password', 'secret');
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle0' });
expect(await page.title()).toContain('Dashboard');
```

**Selenium (Java):**
```java
driver.get("https://example.com/login");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("username"))).sendKeys("user@test.com");
driver.findElement(By.id("password")).sendKeys("secret");
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("button[type='submit']"))).click();
wait.until(ExpectedConditions.urlContains("/dashboard"));
Assertions.assertTrue(driver.getTitle().contains("Dashboard"));
```

## Lifecycle / Setup

- **Puppeteer:** Launch browser, newPage(), run test, browser.close(). Often Mocha/Jest with async.
- **Selenium:** Create WebDriver in @BeforeEach, quit() in @AfterEach. Use WebDriverWait for every interaction that depends on DOM/network; never Thread.sleep.

## Cloud (TestMu)

- Puppeteer: connect via LambdaTest WebSocket (see puppeteer-skill/reference/cloud-integration.md).
- Selenium: RemoteWebDriver + hub + DesiredCapabilities / LT:Options. See [selenium-skill/reference/cloud-integration.md](../../selenium-skill/reference/cloud-integration.md) and [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

## Gotchas

1. **Explicit waits required:** Puppeteer has waitForSelector; Selenium has no auto-wait. Always use WebDriverWait + ExpectedConditions before find + action.
2. **Chrome-only → multi-browser:** Migrating to Selenium allows Firefox, Edge, Safari via different drivers/capabilities.
3. **Language:** Puppeteer is JS/TS only. Selenium supports Java, Python, C#; choose target language and convert control flow and assertions.
4. **Stale handles vs stale elements:** In Puppeteer, ElementHandles can go stale; in Selenium, WebElements can go stale. Prefer storing By locators and re-finding after waits.
5. **Network idle:** Puppeteer's `waitUntil: 'networkidle0'` has no direct Selenium equivalent; use URL or element visibility waits instead.

````


### `reference/selenium-to-cypress.md`

````markdown
# Selenium → Cypress Migration

## API Mapping

### Locators

| Selenium (Java/JS) | Cypress |
|--------------------|---------|
| `By.id("x")` | `cy.get('#x')` |
| `By.cssSelector(".cls")` | `cy.get('.cls')` |
| `By.xpath("//button[text()='Submit']")` | `cy.contains('button', 'Submit')` or `cy.get('button').contains('Submit')` |
| `By.name("q")` | `cy.get('input[name="q"]')` |
| `driver.findElement(By.id("x"))` | `cy.get('#x')` (then chain .click(), .type(), .should()) |

**Cypress:** Prefer `data-cy` or `data-testid`; use `cy.get('[data-cy="x"]')`. No By objects; use selector strings.

### Waits

| Selenium | Cypress |
|----------|---------|
| `WebDriverWait` + `ExpectedConditions.visibilityOfElementLocated(...)` | Not needed; `cy.get(selector).should('be.visible')` or just `cy.get(selector).click()` (Cypress retries) |
| `Thread.sleep(ms)` | **Avoid.** Use `cy.get(...).should(...)` or `cy.wait(alias)` for intercepts |
| Wait for URL | `wait.until(ExpectedConditions.urlContains("/dashboard"))` → `cy.url().should('include', '/dashboard')` |

### Actions

| Selenium | Cypress |
|----------|---------|
| `element.sendKeys("text")` | `cy.get(selector).type('text')` |
| `element.clear()` | `cy.get(selector).clear()` |
| `element.click()` | `cy.get(selector).click()` |
| `new Select(...).selectByValue("v")` | `cy.get('#sel').select('v')` |
| `driver.switchTo().alert().accept()` | `cy.on('window:confirm', () => true)` or stub before action |
| `driver.get(url)` | `cy.visit(url)` or `cy.visit('/path')` with baseURL |

### Assertions

| Selenium | Cypress |
|----------|---------|
| `Assertions.assertTrue(driver.getTitle().contains("X"))` | `cy.title().should('include', 'X')` |
| `Assertions.assertTrue(driver.getCurrentUrl().contains("/dashboard"))` | `cy.url().should('include', '/dashboard')` |
| `Assertions.assertEquals("text", element.getText())` | `cy.get(selector).should('have.text', 'text')` |
| `element.isDisplayed()` | `cy.get(selector).should('be.visible')` |

### Lifecycle

| Selenium | Cypress |
|----------|---------|
| `WebDriver driver = new ChromeDriver();` | No driver in test; Cypress provides `cy` |
| `@BeforeEach` create driver | `beforeEach(() => { cy.visit('/'); });` or similar |
| `driver.quit()` in @AfterEach | No teardown; Cypress manages browser |
| `driver.manage().window().maximize()` | Viewport in cypress.config.js or `cy.viewport(width, height)` |

## Before / After Snippets

**Selenium (Java):**
```java
driver.get("https://example.com/login");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("username"))).sendKeys("user@test.com");
driver.findElement(By.id("password")).sendKeys("secret");
driver.findElement(By.cssSelector("button[type='submit']")).click();
wait.until(ExpectedConditions.urlContains("/dashboard"));
```

**Cypress (JavaScript):**
```javascript
cy.visit('https://example.com/login');
cy.get('#username').type('user@test.com');
cy.get('#password').type('secret');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
```

## Lifecycle / Setup

- **Selenium:** Create WebDriver, run test, quit. Often JUnit/TestNG with @BeforeEach/@AfterEach.
- **Cypress:** No driver creation; use `cy.visit()`, `cy.get()`, etc. in tests. Config in cypress.config.js. Tests are written without async/await (Cypress queues commands).

## Cloud (TestMu)

- Selenium: RemoteWebDriver + hub + capabilities.
- Cypress: LambdaTest Cypress CLI. See [cypress-skill/reference/cloud-integration.md](../../cypress-skill/reference/cloud-integration.md) and [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

## Gotchas

1. **Language:** Selenium may be Java/Python/C#; Cypress is JS/TS only. Migration means rewriting to JavaScript/TypeScript.
2. **No async/await with cy:** Do not use async/await with Cypress commands. Write linear cy chains.
3. **Do not store cy.get() result for later:** Commands are queued and run asynchronously; chain or repeat cy.get() in the same flow.
4. **Explicit waits → retry-ability:** Replace WebDriverWait with Cypress .should(); Cypress automatically retries assertions and get until timeout.
5. **Multiple tabs/windows:** Cypress does not support multiple tabs. If Selenium tests switch windows, consolidate to single tab or document the limitation.

````


### `reference/selenium-to-playwright.md`

````markdown
# Selenium → Playwright Migration

## API Mapping

### Locators

| Selenium (Java/JS) | Playwright (JS/TS) |
|--------------------|--------------------|
| `By.id("x")` | `page.locator('#x')` or `page.getByRole(..., { name: ... })` when semantic |
| `By.cssSelector(".cls")` | `page.locator('.cls')` |
| `By.xpath("//button")` | `page.locator('xpath=//button')` or prefer `page.getByRole('button', { name: '...' })` |
| `By.name("q")` | `page.getByRole('textbox', { name: '...' })` or `page.locator('[name=q]')` |
| `driver.findElement(By.id("x"))` | `page.locator('#x')` (returns Locator, not ElementHandle for most flows) |

**Playwright preference:** Use `getByRole`, `getByLabel`, `getByPlaceholder`, `getByText`, `getByTestId` before falling back to `locator(selector)`.

### Waits

| Selenium | Playwright |
|----------|------------|
| `WebDriverWait` + `ExpectedConditions.elementToBeClickable(By.id("x"))` | `await page.locator('#x').click()` (auto-waits) or `await expect(page.locator('#x')).toBeVisible()` |
| `wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("x")))` | `await expect(page.locator('#x')).toBeVisible()` |
| `Thread.sleep(3000)` | **Avoid.** Use `await expect(locator).toBeVisible()` or `await locator.waitFor()` |
| Implicit wait | Not used; Playwright auto-waits on actions and assertions |

### Actions

| Selenium | Playwright |
|----------|------------|
| `element.sendKeys("text")` | `await locator.fill('text')` or `await locator.pressSequentially('text')` |
| `element.click()` | `await locator.click()` |
| `element.clear()` | `await locator.clear()` or `await locator.fill('')` |
| `new Select(driver.findElement(By.id("sel"))).selectByValue("v")` | `await page.locator('#sel').selectOption('v')` |
| `driver.switchTo().alert().accept()` | `page.on('dialog', d => d.accept())` or handle before action |
| `driver.switchTo().frame("name")` | `await page.frameLocator('iframe[name="name"]')` then use that for locators |
| `driver.switchTo().newWindow(WindowType.TAB)` | `const [newPage] = await Promise.all([context.waitForEvent('page'), page.click('a[target=_blank]')])` |

### Assertions

| Selenium | Playwright |
|----------|------------|
| `Assertions.assertTrue(driver.getTitle().contains("X"))` | `await expect(page).toHaveTitle(/X/)` |
| `Assertions.assertTrue(driver.getCurrentUrl().contains("/dashboard"))` | `await expect(page).toHaveURL(/\/dashboard/)` |
| `Assertions.assertEquals("text", element.getText())` | `await expect(locator).toHaveText('text')` |
| `element.isDisplayed()` | `await expect(locator).toBeVisible()` |

### Lifecycle

| Selenium | Playwright |
|----------|------------|
| `WebDriver driver = new ChromeDriver();` | `const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage();` |
| `driver.get("https://...")` | `await page.goto('https://...')` |
| `driver.quit()` | `await browser.close()` or rely on test runner teardown |
| `driver.manage().window().maximize()` | `await context.setViewportSize({ width: 1920, height: 1080 })` |

## Before / After Snippets

**Selenium (Java):**
```java
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
driver.get("https://example.com/login");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("username"))).sendKeys("user@test.com");
driver.findElement(By.id("password")).sendKeys("secret");
driver.findElement(By.cssSelector("button[type='submit']")).click();
wait.until(ExpectedConditions.urlContains("/dashboard"));
Assertions.assertTrue(driver.getTitle().contains("Dashboard"));
```

**Playwright (TypeScript):**
```typescript
await page.goto('https://example.com/login');
await page.locator('#username').fill('user@test.com');
await page.locator('#password').fill('secret');
await page.locator('button[type="submit"]').click();
await expect(page).toHaveURL(/\/dashboard/);
await expect(page).toHaveTitle(/Dashboard/);
```

## Lifecycle / Setup

- **Selenium:** One `WebDriver` per test; `@BeforeEach` create driver, `@AfterEach` driver.quit(). With Grid, use `RemoteWebDriver` + `DesiredCapabilities`.
- **Playwright:** Use `test.beforeEach` to get `page` from fixture (or create `browser` → `context` → `page`). Use `playwright.config.ts` and `defineConfig`; `page` and `context` are typically provided by the test runner. No explicit "quit" in test if using built-in fixtures.

## Cloud (TestMu)

- Selenium on TestMu: `RemoteWebDriver` + hub URL + `LT:Options` in capabilities.
- Playwright on TestMu: CDP connection to `wss://cdp.lambdatest.com/playwright?capabilities=...` with `LT:Options`.  
  See [playwright-skill/reference/cloud-integration.md](../../playwright-skill/reference/cloud-integration.md) and [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

## Gotchas

1. **No Thread.sleep:** Playwright uses auto-wait. Use `expect(locator).toBeVisible()` or action that implies wait; never `page.waitForTimeout(ms)` unless debugging.
2. **Language shift:** Selenium Java/Python/C# → Playwright usually means rewriting in TypeScript/JavaScript. Playwright does have Java/Python/C# bindings if you want to keep language.
3. **Locator vs ElementHandle:** Prefer keeping `Locator` and reusing it; Playwright re-resolves on each action. Avoid storing stale element references.
4. **Alerts/dialogs:** In Selenium you switch to alert after it appears. In Playwright, set `page.on('dialog', ...)` before the action that triggers the dialog.
5. **Multiple windows:** Playwright uses `context.waitForEvent('page')` or `page.context().pages()`; there is no direct `switchTo().window(handle)`.

````


### `reference/selenium-to-puppeteer.md`

````markdown
# Selenium → Puppeteer Migration

## API Mapping

### Locators

| Selenium (Java/JS) | Puppeteer |
|--------------------|-----------|
| `By.id("x")` | `await page.$('#x')` or `page.$('#x')` (returns ElementHandle) |
| `By.cssSelector(".cls")` | `await page.$('.cls')` |
| `By.xpath("//button")` | `await page.$('xpath///button')` or use CSS equivalent |
| `driver.findElement(By.id("x"))` | `await page.$('#x')` (then use handle for actions) |
| `driver.findElements(By.cssSelector(".item"))` | `await page.$$('.item')` |

**Note:** Puppeteer uses CSS by default; for XPath use `page.$('xpath///...')`. Prefer `page.$` for single element, `page.$$` for array.

### Waits

| Selenium | Puppeteer |
|----------|-----------|
| `WebDriverWait` + `ExpectedConditions.visibilityOfElementLocated(...)` | `await page.waitForSelector('.selector', { visible: true, timeout: 10000 })` |
| `wait.until(ExpectedConditions.elementToBeClickable(...))` | `await page.waitForSelector('button', { visible: true }); await page.click('button');` |
| `Thread.sleep(ms)` | **Avoid.** Use `await page.waitForSelector(...)` or `await page.waitForFunction(...)` |
| Wait for navigation | `await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle0' }), page.click('a')]);` |

### Actions

| Selenium | Puppeteer |
|----------|-----------|
| `element.sendKeys("text")` | `await page.type('#selector', 'text')` or `await element.type('text')` (deprecated: use `element.type` on handle) — prefer `await page.$('#x')` then `await (await page.$('#x')).type('text')` or `page.evaluate` for complex cases. For modern Puppeteer use `await page.evaluate(el => { el.value = 'text'; })` or `await (await page.$('#x')).type('text', { delay: 0 });` |
| `element.click()` | `await page.click('#selector')` or `await (await page.$('#selector')).click()` |
| `element.clear()` | `await page.evaluate(sel => { document.querySelector(sel).value = ''; }, '#x')` or type empty after select all |
| Select dropdown | `await page.select('#sel', 'value')` |
| Alert accept | `page.on('dialog', d => d.accept());` before triggering action |
| Switch to frame | `const frame = page.frames().find(f => f.name() === 'name');` then use `frame.$`, `frame.click`, etc. |

### Assertions

| Selenium | Puppeteer |
|----------|-----------|
| `driver.getTitle().contains("X")` | `const title = await page.title();` then assert in test (e.g. `expect(title).toContain('X')`) |
| `driver.getCurrentUrl().includes("/dashboard")` | `page.url()` or `page.url()` then assert |
| `element.getText()` | `await page.evaluate(el => el.textContent, await page.$('#x'))` or `await (await page.$('#x')).evaluate(el => el.textContent)` |

### Lifecycle

| Selenium | Puppeteer |
|----------|-----------|
| `WebDriver driver = new ChromeDriver();` | `const browser = await puppeteer.launch(); const page = await browser.newPage();` |
| `driver.get("https://...")` | `await page.goto('https://...', { waitUntil: 'networkidle0' });` |
| `driver.quit()` | `await browser.close();` |
| `driver.manage().window().maximize()` | `await page.setViewport({ width: 1920, height: 1080 });` |

## Before / After Snippets

**Selenium (Java):**
```java
driver.get("https://example.com/login");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("username"))).sendKeys("user@test.com");
driver.findElement(By.id("password")).sendKeys("secret");
driver.findElement(By.cssSelector("button[type='submit']")).click();
```

**Puppeteer (JavaScript):**
```javascript
await page.goto('https://example.com/login', { waitUntil: 'networkidle0' });
await page.type('#username', 'user@test.com');
await page.type('#password', 'secret');
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle0' });
```

## Lifecycle / Setup

- **Selenium:** One WebDriver per test; teardown with `driver.quit()`.
- **Puppeteer:** `puppeteer.launch()` once (or per test); create `page` with `browser.newPage()`. Always `browser.close()` in finally/teardown. Typically no test runner; wrap in async IIFE or use Mocha/Jest with async tests.

## Cloud (TestMu)

- Selenium on TestMu: RemoteWebDriver + hub + capabilities.
- Puppeteer on TestMu: Connect via WebSocket endpoint (LambdaTest Puppeteer integration). See [puppeteer-skill/reference/cloud-integration.md](../../puppeteer-skill/reference/cloud-integration.md) and [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

## Gotchas

1. **Chrome/Chromium only:** Puppeteer does not drive Firefox or Safari; migration implies Chrome-only.
2. **Language:** Selenium may be Java/Python/C#; Puppeteer is JS/TS only. Migration usually means rewriting to JavaScript/TypeScript.
3. **No auto-wait:** Unlike Playwright, Puppeteer does not auto-wait. Use `waitForSelector`, `waitForNavigation`, or `waitForFunction` explicitly.
4. **page.type vs fill:** `page.type()` sends key events; for clearing + typing use `page.evaluate` to set value or type with delay. Newer patterns prefer `page.evaluate` for large text or `elementHandle.type()`.
5. **ElementHandle can go stale:** After navigation or DOM change, re-query with `page.$()`; do not reuse old handles.

````
