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
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')
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
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