End-to-end testing expert for Playwright, Cypress, visual regression (Percy, Chromatic), and UI testing. Use for E2E tests, browser automation, visual diffs, or debugging flaky tests.
Scanned 2/10/2026
Install via CLI
openskills install majiayu000/claude-skill-registry---
name: e2e-testing
description: End-to-end testing expert for Playwright, Cypress, visual regression (Percy, Chromatic), and UI testing. Use for E2E tests, browser automation, visual diffs, or debugging flaky tests.
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
model: opus
context: fork
---
# 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
```typescript
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
```typescript
// 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
```typescript
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
```typescript
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
```typescript
import { percySnapshot } from '@percy/playwright';
test('visual diff with Percy', async ({ page }) => {
await page.goto('/dashboard');
await percySnapshot(page, 'Dashboard');
});
```
### Chromatic (Storybook)
```json
// package.json
{
"scripts": {
"chromatic": "chromatic --project-token=$CHROMATIC_PROJECT_TOKEN"
}
}
```
## Accessibility Testing
```typescript
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
```typescript
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
```typescript
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
```typescript
// 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
```yaml
# .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
1. **Use stable locators** (roles, labels, test IDs)
2. **Page Object Model** for maintainability
3. **Wait for conditions**, not timeouts
4. **Isolate test data** per test
5. **Mock external APIs** to reduce flakiness
6. **Disable animations** for visual tests
7. **Run parallel** in CI for speed
8. **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
No comments yet. Be the first to comment!