E2E Testing Expert - Playwright, Visual Regression, UI Testing
Core Expertise
-
Playwright/Cypress for browser automation
-
Visual regression with Percy, Chromatic, BackstopJS
-
UI testing with Testing Library patterns
-
Accessibility testing with axe-core
-
Mobile emulation and device testing
Playwright Fundamentals
Test Structure
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => { test('should login successfully', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').fill('password123'); await page.getByRole('button', { name: 'Login' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
}); });
Page Object Model
// pages/LoginPage.ts export class LoginPage { constructor(private page: Page) {}
readonly emailInput = this.page.getByLabel('Email'); readonly passwordInput = this.page.getByLabel('Password'); readonly loginButton = this.page.getByRole('button', { name: 'Login' });
async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.loginButton.click(); } }
Fixtures
import { test as base } from '@playwright/test'; import { LoginPage } from './pages/LoginPage';
export const test = base.extend<{ loginPage: LoginPage }>({ loginPage: async ({ page }, use) => { const loginPage = new LoginPage(page); await loginPage.goto(); await use(loginPage); }, });
Visual Regression
Playwright Screenshots
test('homepage matches baseline', async ({ page }) => { await page.goto('/'); await expect(page).toHaveScreenshot('homepage.png', { fullPage: true, animations: 'disabled', }); });
// Responsive screenshots await page.setViewportSize({ width: 1920, height: 1080 }); await expect(page).toHaveScreenshot('desktop.png');
await page.setViewportSize({ width: 375, height: 667 }); await expect(page).toHaveScreenshot('mobile.png');
Percy Integration
import { percySnapshot } from '@percy/playwright';
test('visual diff with Percy', async ({ page }) => { await page.goto('/dashboard'); await percySnapshot(page, 'Dashboard'); });
Chromatic (Storybook)
// package.json { "scripts": { "chromatic": "chromatic --project-token=$CHROMATIC_PROJECT_TOKEN" } }
Accessibility Testing
import AxeBuilder from '@axe-core/playwright';
test('accessibility audit', async ({ page }) => { await page.goto('/');
const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa']) .analyze();
expect(results.violations).toEqual([]); });
// Keyboard navigation test('keyboard navigation', async ({ page }) => { await page.goto('/form'); await page.keyboard.press('Tab'); await expect(page.getByLabel('Email')).toBeFocused(); });
Mobile Testing
import { devices } from '@playwright/test';
test.use(devices['iPhone 13 Pro']);
test('mobile navigation', async ({ page }) => { await page.goto('/'); await expect(page.getByRole('button', { name: 'Menu' })).toBeVisible(); });
Network Mocking
test('mock API response', async ({ page }) => { await page.route('/api/users', async (route) => { await route.fulfill({ status: 200, body: JSON.stringify([{ id: 1, name: 'John' }]), }); });
await page.goto('/users'); await expect(page.getByText('John')).toBeVisible(); });
Debugging Flaky Tests
// Proper waiting (NOT setTimeout) await page.waitForLoadState('networkidle'); await page.waitForSelector('.content', { state: 'visible' });
// Retry configuration export default defineConfig({ retries: process.env.CI ? 2 : 0, use: { trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, });
CI/CD Configuration
.github/workflows/e2e.yml
name: E2E Tests on: [push]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npx playwright install --with-deps - run: npm run test:e2e - uses: actions/upload-artifact@v3 if: failure() with: name: playwright-report path: playwright-report/
Best Practices
-
Use stable locators (roles, labels, test IDs)
-
Page Object Model for maintainability
-
Wait for conditions, not timeouts
-
Isolate test data per test
-
Mock external APIs to reduce flakiness
-
Disable animations for visual tests
-
Run parallel in CI for speed
-
Save traces/screenshots on failure
Test Organization
e2e/ ├── fixtures/ │ └── auth.fixture.ts ├── pages/ │ ├── LoginPage.ts │ └── DashboardPage.ts ├── tests/ │ ├── auth.spec.ts │ └── dashboard.spec.ts └── playwright.config.ts
Related Skills
-
qa-engineer
-
Overall test strategy
-
unit-testing
-
Unit and integration tests