E2E Testing Guide

Playwright vs Cypress

FeaturePlaywrightCypress
BrowsersChromium, Firefox, WebKit (Safari)Chromium, Firefox, Electron
Multi-tab / Multi-windowYesLimited
LanguageJS/TS, Python, Java, C#JS/TS only
ArchitectureOut-of-process (CDP/WebDriver BiDi)In-process (runs in browser)
Auto-waitingYes (built-in retries)Yes (command chaining)
Network interceptionYes (route/fulfill)Yes (cy.intercept)
Component testingExperimentalMature (@cypress/react)
Parallel executionBuilt-in shardingRequires Cypress Cloud (paid)
Best forCross-browser, complex flowsReact/Vue/Angular component + E2E

Playwright: Page Object Model

// pages/LoginPage.ts import { Page, Locator } from '@playwright/test'; export class LoginPage { readonly emailInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; readonly errorMessage: Locator; constructor(private page: Page) { this.emailInput = page.getByLabel('Email'); this.passwordInput = page.getByLabel('Password'); this.submitButton = page.getByRole('button', { name: 'Log In' }); this.errorMessage = page.getByRole('alert'); } async goto() { await this.page.goto('/login'); } async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.submitButton.click(); } } // tests/login.spec.ts import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; test.describe('Login', () => { test('successful login redirects to dashboard', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('user@example.com', 'correct123'); await expect(page).toHaveURL('/dashboard'); await expect(page.getByText('Welcome back')).toBeVisible(); }); test('failed login shows error message', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('user@example.com', 'wrong'); await expect(loginPage.errorMessage).toContainText('Invalid'); }); });

Avoiding Flaky Tests

Anti-PatternFix
Hard-coded sleep(1000)Use await expect(locator).toBeVisible() — auto-waits
Selecting by CSS classUse data-testid or ARIA roles — stable selectors
Test order dependenciesEach test sets up its own state; teardown after
Real third-party callsMock/stub external APIs with route interception
Shared test dataCreate unique data per test (timestamp, random ID)
Scrolling/animation timingDisable animations in test env (prefers-reduced-motion)