Comparisons

Playwright vs Cypress: AI Rules for Each

Playwright and Cypress are both E2E testing frameworks but differ in architecture, auto-waiting, multi-browser support, and API. Each needs specific AI rules to prevent the AI from mixing their assertion patterns and selector strategies.

7 min readยทJune 17, 2025

cy.get() in Playwright = undefined. page.locator() in Cypress = undefined. Every E2E line is framework-specific.

Selectors, assertions, test structure, multi-browser, parallel execution, and when to choose each framework

Different Architectures, Different Test Patterns

Playwright and Cypress are both browser-based E2E testing frameworks, but their architectures produce different test patterns. Playwright: runs tests outside the browser (Node.js process controls browsers via WebSocket). Supports: Chromium, Firefox, and WebKit (Safari). Tests use: async/await, page.locator() for finding elements, and expect() for assertions. Playwright is: multi-browser by default, auto-waits for elements, and runs tests in parallel across browsers.

Cypress: runs tests inside the browser (the test code executes in the same JavaScript context as the application). Primarily supports: Chromium-based browsers (Chrome, Edge). Tests use: cy.get() chainable commands, .should() for assertions, and a command queue that executes sequentially. Cypress is: single-browser focused, auto-retries assertions, and provides a visual test runner with time-travel debugging.

Without E2E framework rules: the AI generates page.locator() in a Cypress project (Playwright API, does not exist in Cypress), cy.get() in a Playwright project (Cypress API, does not exist in Playwright), .should('be.visible') in Playwright (Cypress assertion syntax), or expect(locator).toBeVisible() in Cypress (Playwright assertion). The E2E framework determines: every selector, every assertion, and every test structure pattern. One rule prevents: every cross-framework syntax error.

Selectors and Waiting: Locators vs cy.get

Playwright selectors: page.locator() with auto-waiting. const button = page.getByRole('button', { name: 'Submit' }). await button.click(). Playwright auto-waits: for the element to be visible, enabled, and stable before interacting. No explicit waits needed. Preferred selectors: getByRole (accessibility-based), getByText, getByLabel, getByPlaceholder, getByTestId. AI rule: 'Playwright: use page.getByRole, getByText, getByLabel for selectors. Auto-wait handles timing. No cy.wait() or explicit sleeps.'

Cypress selectors: cy.get() with CSS selectors or data attributes. cy.get('[data-testid="submit-btn"]').click(). cy.get() retries: automatically until the element is found (default 4-second timeout). Chaining: cy.get('form').find('input[name=email]').type('test@example.com'). Cypress assertions: .should('be.visible'), .should('have.text', 'Submit'). AI rule: 'Cypress: use cy.get with data-testid attributes. Chain commands: cy.get().find().type(). Assertions: .should("be.visible"), .should("contain", "text").'

The selector rule prevents: the AI using cy.get('[data-testid=...]') in Playwright (use page.getByTestId instead), using page.getByRole() in Cypress (not a Cypress API), or mixing .should() and expect() assertions. The selector and assertion APIs are: completely different between the two frameworks despite solving the same problem. One rule about which framework and which selectors: prevents every test syntax error.

  • Playwright: page.getByRole/getByText/getByLabel + auto-wait + expect() assertions
  • Cypress: cy.get/cy.contains + retry + .should() assertions + command chaining
  • Playwright preferred: accessibility selectors (getByRole). Cypress preferred: data-testid
  • Playwright: async/await on every action. Cypress: command queue, no await needed
  • AI mixing: cy.get in Playwright = undefined. page.locator in Cypress = undefined
๐Ÿ’ก Auto-Wait vs Retry: Same Goal, Different API

Playwright auto-waits: page.getByRole('button').click() waits for the button to be visible, enabled, and stable. Cypress retries: cy.get('[data-testid=btn]') retries until the element is found (4s default). Both solve flaky tests. The API is completely different. One rule prevents: every selector and timing error.

Test Structure: test.describe vs describe/it

Playwright test structure: import { test, expect } from '@playwright/test'. test.describe('Login', () => { test('user can login with valid credentials', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('test@example.com'); await page.getByLabel('Password').fill('password'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page.getByText('Dashboard')).toBeVisible(); }); }). Key: async function with page fixture, await on every action, expect() for assertions.

Cypress test structure: describe('Login', () => { it('user can login with valid credentials', () => { cy.visit('/login'); cy.get('[data-testid=email]').type('test@example.com'); cy.get('[data-testid=password]').type('password'); cy.get('[data-testid=submit]').click(); cy.contains('Dashboard').should('be.visible'); }); }). Key: synchronous-looking code (no async/await), cy.visit instead of page.goto, cy.contains for text matching, .should() for assertions.

The structure rule prevents: the AI using async ({ page }) in Cypress tests (Cypress does not use page fixtures or async/await for commands), using cy.visit in Playwright (use page.goto), generating .should('be.visible') in Playwright (use expect(locator).toBeVisible()), or importing from '@playwright/test' in a Cypress file. The test file structure, import, and assertion API are: all framework-specific. Every line of an E2E test: depends on knowing which framework is in use.

  • Playwright: import { test, expect } from '@playwright/test'. async ({ page }) => { await... }
  • Cypress: describe/it blocks, no imports for cy. Synchronous command queue, no await
  • Navigation: Playwright page.goto(). Cypress cy.visit()
  • Text: Playwright page.getByText(). Cypress cy.contains()
  • Assertions: Playwright expect(locator).toBeVisible(). Cypress .should('be.visible')
โš ๏ธ Every Line Is Framework-Specific

Playwright: await page.goto('/login'). Cypress: cy.visit('/login'). Playwright: await expect(page.getByText('Dashboard')).toBeVisible(). Cypress: cy.contains('Dashboard').should('be.visible'). Same test, zero shared syntax. The AI must know: which framework to generate for. One rule in CLAUDE.md: prevents every line from being wrong.

Multi-Browser and Parallel Execution

Playwright multi-browser: tests run on Chromium, Firefox, AND WebKit by default. Configuration: playwright.config.ts projects array defines browser targets. One test file: runs 3 times (once per browser). Cross-browser bugs: caught automatically. Parallel: Playwright runs test files in parallel by default (configurable workers count). AI rule: 'Playwright: tests run on all browsers. Write browser-agnostic selectors (getByRole, not browser-specific CSS). Parallel by default โ€” tests must not share state between files.'

Cypress multi-browser: primarily Chromium (Chrome, Edge). Firefox support: experimental. WebKit (Safari): not supported natively. Cypress runs: one browser at a time. Cross-browser testing: requires separate CI runs per browser. Parallel: Cypress Dashboard (paid) or splitting spec files across CI machines. AI rule: 'Cypress: Chromium browsers primary. Cross-browser: configure CI matrix for Chrome + Firefox. Parallel: split spec files across CI jobs or use Cypress Dashboard.'

The multi-browser rule matters for AI because: Playwright tests should be browser-agnostic (do not use Chrome-specific selectors or APIs). Cypress tests: can use Chromium-specific patterns because that is the primary target. If the AI generates a Playwright test with a Chrome-specific selector: it passes on Chromium but fails on Firefox/WebKit. The rule ensures: Playwright tests work on all browsers, Cypress tests work on Chromium.

When to Choose Each Framework

Choose Playwright when: you need cross-browser testing (Chromium + Firefox + WebKit in one test run), you prefer async/await test style (familiar to most TypeScript developers), you want parallel execution out of the box (faster CI), your project is Next.js (Playwright has first-party Next.js integration), or you need to test in WebKit/Safari (only Playwright supports this). Playwright is: the 2026 default recommendation for new E2E testing setups.

Choose Cypress when: your team already uses Cypress (migration cost is real โ€” rewriting E2E tests is expensive), you value the visual test runner (Cypress's time-travel debugging is unmatched for development-time debugging), your tests only need Chromium (Safari testing is not required), or you prefer the command-chaining API style (some developers find cy.get().type().should() more readable than await/expect). Cypress is: an excellent tool with a large community, stronger for Chromium-only testing.

For new projects in 2026: Playwright is the recommended default. Multi-browser, parallel, async/await, and Next.js integration make it: the most capable E2E framework. For existing Cypress projects: migrating is rarely worth the cost unless you specifically need multi-browser or WebKit testing. Both frameworks: produce high-quality E2E tests when the AI generates correct framework-specific patterns. The rule file determines: which patterns the AI generates.

  • Playwright: multi-browser, async/await, parallel, Next.js integration โ€” 2026 default for new projects
  • Cypress: visual debugger, command chaining, Chromium-focused โ€” strong for existing setups
  • New project: Playwright (more capable). Existing Cypress: keep (migration cost exceeds benefit)
  • WebKit/Safari testing: only Playwright supports this natively
  • Both: excellent E2E frameworks. The AI rule determines: which patterns to generate
โ„น๏ธ Playwright = 2026 Default for New Projects

Multi-browser (Chromium + Firefox + WebKit), parallel by default, async/await style, Next.js first-party integration. Playwright covers: the most browsers, runs the fastest in CI, and matches modern TypeScript conventions. Existing Cypress: keep it (migration cost > benefit). New setup: Playwright.

E2E Framework Rule Summary

Summary of Playwright vs Cypress AI rules.

  • Selectors: Playwright getByRole/getByText (auto-wait) vs Cypress cy.get (retry, data-testid)
  • Assertions: Playwright expect(locator).toBeVisible() vs Cypress .should('be.visible')
  • Structure: Playwright async ({ page }) + await vs Cypress describe/it + synchronous commands
  • Navigation: Playwright page.goto() vs Cypress cy.visit()
  • Browsers: Playwright = Chromium + Firefox + WebKit. Cypress = Chromium primary
  • Parallel: Playwright parallel by default. Cypress: Dashboard (paid) or CI splitting
  • 2026 default: Playwright for new projects. Cypress for existing setups (keep, do not migrate)
  • AI mixing: every selector, assertion, and structure pattern is framework-specific โ€” one rule prevents all errors