Migration and Comparison
Most teams do not start from zero — they have existing Selenium or Cypress suites that they need to evaluate, maintain, or migrate. Understanding the practical trade-offs between frameworks, and having a migration strategy, is essential knowledge for QA engineers.
Framework Comparison
| Feature | Playwright | Selenium | Cypress |
|---|---|---|---|
| Architecture | WebSocket to browser | HTTP to driver process | Runs inside browser |
| Languages | TS/JS, Python, Java, C# | Java, Python, JS, C#, Ruby, Go | JavaScript/TypeScript only |
| Browsers | Chromium, Firefox, WebKit | Chrome, Firefox, Safari, Edge, IE | Chromium, Firefox, WebKit (limited) |
| Auto-waiting | Built-in | Manual (explicit/implicit waits) | Built-in |
| Network mocking | Native page.route() |
Proxy-based (complex setup) | Native cy.intercept() |
| Parallel execution | Built-in workers + sharding | Selenium Grid (separate infra) | Cypress Cloud (paid) or DIY |
| Multi-tab support | Native | Native | Not supported |
| iFrame support | frameLocator() |
switch_to.frame() |
cy.iframe() (plugin) |
| Test runner | Built-in | Bring your own | Built-in (Mocha-based) |
| Mobile testing | Device emulation | Appium integration | Device emulation |
| Debugging | Trace Viewer, UI Mode | Browser DevTools | Time-travel debugger |
When to Choose What
Choose Playwright When
- Starting a new project with no existing test suite
- Team uses TypeScript, Python, Java, or C#
- Need cross-browser testing (including WebKit/Safari engine)
- Need network mocking, multi-tab, or advanced browser control
- Want built-in test infrastructure (runner, reporter, traces)
Keep Selenium When
- Large existing Selenium suite with years of investment
- Team uses Ruby, Go, or other languages Playwright does not support
- Need real Safari testing (not WebKit approximation)
- Organization mandates Selenium or uses Selenium Grid infrastructure
- Integrating with Appium for mobile native testing
Choose Cypress When
- Team is exclusively JavaScript/TypeScript
- Testing a single-page application
- Developer experience and fast feedback loop are the priority
- Do not need multi-tab or multi-origin testing
Migrating from Selenium to Playwright
Step 1: Concept Mapping
| Selenium Concept | Playwright Equivalent |
|---|---|
WebDriver / driver |
Page / page |
driver.get(url) |
page.goto(url) |
driver.find_element(By.ID, 'x') |
page.locator('#x') or page.getByTestId('x') |
element.click() |
locator.click() (with auto-wait) |
element.send_keys('text') |
locator.fill('text') |
WebDriverWait + expected_conditions |
Auto-waiting (built-in) |
driver.switch_to.frame() |
page.frameLocator() |
driver.quit() |
Context/browser cleanup (automatic in test runner) |
| Selenium Grid | Built-in workers + sharding |
Page Object with WebDriverWait |
Page Object with Locators (no waits needed) |
Step 2: Migration Strategy
Approach A: Gradual (Recommended)
- New tests written in Playwright from day one
- Run both suites in CI in parallel
- Migrate existing tests by priority (critical paths first)
- Decommission Selenium suite when coverage is equivalent
Approach B: Big Bang
- Rewrite all tests in Playwright at once
- Faster transition but higher risk
- Only viable for small suites (< 100 tests)
Step 3: Common Conversion Patterns
# Selenium
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-testid="submit"]'))
)
element.click()
# Playwright — no explicit wait needed
page.get_by_test_id("submit").click()
// Selenium (JS)
await driver.wait(until.elementLocated(By.id('result')), 10000);
const text = await driver.findElement(By.id('result')).getText();
assert.equal(text, 'Success');
// Playwright — web-first assertion retries automatically
await expect(page.locator('#result')).toHaveText('Success');
Migrating from Cypress to Playwright
| Cypress Pattern | Playwright Equivalent |
|---|---|
cy.visit('/page') |
await page.goto('/page') |
cy.get('[data-cy="x"]') |
page.getByTestId('x') |
cy.contains('text') |
page.getByText('text') |
cy.intercept() |
page.route() |
cy.fixture('data.json') |
Custom fixture or import |
beforeEach() |
test.beforeEach() or fixtures |
| Cypress Cloud (parallel) | Built-in sharding |
Key Differences to Watch
- Cypress commands are automatically chained and retried; Playwright uses
async/awaitexplicitly - Cypress runs inside the browser (single-origin limitation); Playwright controls from outside (multi-origin supported)
- Cypress has no multi-tab support; Playwright handles tabs natively
Key Takeaways
- Playwright offers the best combination of speed, reliability, and features for new projects
- Selenium remains valid for teams with large existing investments or unsupported language requirements
- Cypress is strong for SPA-focused JavaScript teams but has architectural limitations
- Gradual migration (new tests in Playwright, migrate existing by priority) is the safest approach
- Most Selenium patterns have simpler Playwright equivalents — especially waits and locators