Why Playwright Wins in 2026
Playwright is the dominant browser testing framework, surpassing Cypress in adoption. The key advantages: auto-waiting (no flaky sleeps), multi-browser support (Chromium, Firefox, Safari), parallel execution, component testing, visual regression, API testing, and built-in code generation.
Unlike Cypress (which runs inside the browser), Playwright controls browsers via the DevTools protocol — giving it capabilities Cypress cannot match: multi-tab testing, file downloads, iframe support, and background page testing.
Playwright 1.49 adds: improved component testing, enhanced auto-waiting for web components, better trace viewer, and faster parallel execution.
Key Takeaways
Writing Resilient Tests with Locators
Playwright's locator API is designed to be resilient: locators auto-wait for elements, retry failed assertions, and filter elements precisely. The recommended locator priority mirrors Testing Library: getByRole > getByLabel > getByText > getByTestId.
The key difference from Cypress: Playwright locators are lazy — they find elements when an action is performed, not when the locator is created. This means your locator always refers to the latest DOM state.
import { test, expect } from '@playwright/test'; test.describe('Blog Navigation', () => { test('user can read a blog post', async ({ page }) => { await page.goto('/blog'); // Click on a blog post by its heading text await page.getByRole('heading', { name: /react server components/i }).click(); // Auto-waits for navigation and content to load await expect(page).toHaveURL(/\/blog\/react-server-components/); await expect( page.getByRole('heading', { level: 1 }) ).toContainText('React'); // Verify article content is present await expect(page.getByRole('article')).toBeVisible(); await expect(page.getByText('Server Components')).toBeVisible(); }); test('user can filter posts by category', async ({ page }) => { await page.goto('/blog'); // Click category filter await page.getByRole('button', { name: /frontend/i }).click(); // Verify only frontend posts are shown const posts = page.getByRole('article'); await expect(posts).toHaveCount(5); // 5 frontend posts // Each post should have the Frontend category badge for (const post of await posts.all()) { await expect(post.getByText('Frontend')).toBeVisible(); } }); test('user can search for posts', async ({ page }) => { await page.goto('/blog'); // Type in search box await page.getByPlaceholder(/search/i).fill('typescript'); // Verify filtered results await expect(page.getByRole('article')).toHaveCount(1); await expect( page.getByText(/typescript generics/i) ).toBeVisible(); }); });
Key Takeaways
Visual Regression Testing
Visual regression testing captures screenshots and compares them against baselines. Any pixel difference triggers a test failure. This catches CSS regressions, layout shifts, and styling bugs that functional tests miss entirely.
Playwright has built-in visual comparison: toHaveScreenshot() captures the page and compares against a stored baseline. On first run, it stores the baseline. On subsequent runs, it detects differences.
test('blog listing page visual regression', async ({ page }) => { await page.goto('/blog'); // Wait for all images and animations to complete await page.waitForLoadState('networkidle'); // Full page screenshot comparison await expect(page).toHaveScreenshot('blog-listing.png', { maxDiffPixelRatio: 0.01, // Allow 1% pixel difference fullPage: true, }); }); test('blog post visual regression', async ({ page }) => { await page.goto('/blog/react-server-components-guide'); await page.waitForLoadState('networkidle'); // Compare specific element (not full page) const article = page.getByRole('article'); await expect(article).toHaveScreenshot('rsc-article.png', { maxDiffPixelRatio: 0.02, }); }); test('responsive design — mobile view', async ({ page }) => { // Test mobile viewport await page.setViewportSize({ width: 375, height: 812 }); await page.goto('/blog'); await page.waitForLoadState('networkidle'); await expect(page).toHaveScreenshot('blog-mobile.png', { maxDiffPixelRatio: 0.01, }); });
API Testing with Playwright
Playwright is not just for browser testing — it has a powerful API testing client (request context) that can test REST APIs directly. This is useful for: testing API endpoints before building the UI, seeding test data, and verifying server-side behavior.
Combine API testing with browser testing: use the API to create test data, then use the browser to verify the data renders correctly. This creates realistic end-to-end flows without slow UI-based data setup.
import { test, expect } from '@playwright/test'; test.describe('Blog API', () => { test('GET /api/posts returns blog posts', async ({ request }) => { const response = await request.get('/api/posts'); expect(response.ok()).toBeTruthy(); expect(response.status()).toBe(200); const posts = await response.json(); expect(posts.length).toBeGreaterThan(0); expect(posts[0]).toHaveProperty('title'); expect(posts[0]).toHaveProperty('slug'); }); test('POST /api/posts creates a new post', async ({ request }) => { const response = await request.post('/api/posts', { data: { title: 'Test Post from Playwright', content: 'This is a test post created during E2E testing.', category: 'Frontend', }, headers: { Authorization: 'Bearer test-token', }, }); expect(response.status()).toBe(201); const post = await response.json(); expect(post.title).toBe('Test Post from Playwright'); }); }); // Combined: API setup + Browser verification test('newly created post appears on blog page', async ({ page, request }) => { // 1. Create post via API (fast) const response = await request.post('/api/posts', { data: { title: 'E2E Test Post', content: 'Content', category: 'QA' }, headers: { Authorization: 'Bearer test-token' }, }); // 2. Verify in browser (the real test) await page.goto('/blog'); await expect(page.getByText('E2E Test Post')).toBeVisible(); });
Key Takeaways
CI Integration & Parallel Execution
Playwright is designed for CI from the start. It supports parallel test execution (split tests across workers), sharding (split tests across CI machines), retries (retry failed tests automatically), and HTML reports with trace viewer.
In CI, use the built-in GitHub Actions workflow that installs browser binaries, runs tests, and uploads the HTML report as an artifact.
// playwright.config.ts — Production Configuration import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, // Prevent .only in CI retries: process.env.CI ? 2 : 0, // Retry twice in CI workers: process.env.CI ? 4 : undefined, // 4 parallel workers in CI reporter: process.env.CI ? [['html', { open: 'never' }], ['github']] : [['html', { open: 'on-failure' }]], use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', // Record trace on failure for debugging screenshot: 'only-on-failure', video: 'retain-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, { name: 'mobile', use: { ...devices['iPhone 14'] } }, ], webServer: { command: 'pnpm dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, }); // .github/workflows/e2e.yml // - uses: actions/checkout@v4 // - uses: actions/setup-node@v4 // - run: pnpm install --frozen-lockfile // - run: pnpm exec playwright install --with-deps // - run: pnpm exec playwright test // - uses: actions/upload-artifact@v4 // if: always() // with: // name: playwright-report // path: playwright-report/
Key Takeaways
Key Takeaways
Playwright is the complete testing toolkit: E2E browser tests, component tests, API tests, and visual regression — all in one framework. Its auto-waiting engine eliminates flaky tests, and parallel execution keeps test suites fast.
For interviews: explain auto-waiting vs explicit waits, demonstrate the locator priority (role > label > text > testid), discuss visual regression testing trade-offs, and show how to combine API setup with browser verification for fast, realistic tests.