Claude-skill-registry e2e-playwright
Playwright end-to-end testing expert. Use when writing E2E tests, browser automation, or debugging flaky tests.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/e2e-playwright" ~/.claude/skills/majiayu000-claude-skill-registry-e2e-playwright && rm -rf "$T"
manifest:
skills/data/e2e-playwright/SKILL.mdsource content
E2E Playwright Testing Expert
Core Expertise
1. Playwright Fundamentals
Browser Automation:
- Multi-browser support (Chromium, Firefox, WebKit)
- Context isolation and parallel execution
- Auto-waiting and actionability checks
- Network interception and mocking
- File downloads and uploads
- Geolocation and permissions
- Authentication state management
Test Structure:
import { test, expect } from '@playwright/test'; test.describe('Authentication Flow', () => { test.beforeEach(async ({ page }) => { await page.goto('/login'); }); test('should login successfully', async ({ page }) => { 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 back')).toBeVisible(); }); test('should show validation errors', async ({ page }) => { await page.getByRole('button', { name: 'Login' }).click(); await expect(page.getByText('Email is required')).toBeVisible(); await expect(page.getByText('Password is required')).toBeVisible(); }); });
2. Page Object Model (POM)
Pattern: Encapsulate page interactions for maintainability
// pages/LoginPage.ts import { Page, Locator } from '@playwright/test'; export class LoginPage { readonly page: Page; readonly emailInput: Locator; readonly passwordInput: Locator; readonly loginButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page = page; this.emailInput = page.getByLabel('Email'); this.passwordInput = page.getByLabel('Password'); this.loginButton = page.getByRole('button', { name: 'Login' }); 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.loginButton.click(); } async loginWithGoogle() { await this.page.getByRole('button', { name: 'Continue with Google' }).click(); // Handle OAuth popup } async expectError(message: string) { await expect(this.errorMessage).toContainText(message); } } // Usage in tests test('login flow', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('user@example.com', 'password123'); await expect(page).toHaveURL('/dashboard'); });
3. Test Fixtures & Custom Contexts
Fixtures: Reusable setup/teardown logic
// fixtures/auth.fixture.ts import { test as base } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; type AuthFixtures = { authenticatedPage: Page; loginPage: LoginPage; }; export const test = base.extend<AuthFixtures>({ authenticatedPage: async ({ page }, use) => { // Setup: Login before test 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 page.waitForURL('/dashboard'); await use(page); // Teardown: Logout after test await page.getByRole('button', { name: 'Logout' }).click(); }, loginPage: async ({ page }, use) => { const loginPage = new LoginPage(page); await loginPage.goto(); await use(loginPage); }, }); export { expect } from '@playwright/test'; // Usage test('authenticated user can view profile', async ({ authenticatedPage }) => { await authenticatedPage.goto('/profile'); await expect(authenticatedPage.getByText('Profile Settings')).toBeVisible(); });
4. API Testing with Playwright
Pattern: Test backend APIs alongside E2E flows
import { test, expect } from '@playwright/test'; test.describe('API Testing', () => { test('should fetch user data', async ({ request }) => { const response = await request.get('/api/users/123'); expect(response.ok()).toBeTruthy(); expect(response.status()).toBe(200); const data = await response.json(); expect(data).toMatchObject({ id: 123, email: expect.any(String), name: expect.any(String), }); }); test('should handle authentication', async ({ request }) => { const response = await request.post('/api/auth/login', { data: { email: 'user@example.com', password: 'password123', }, }); expect(response.ok()).toBeTruthy(); const { token } = await response.json(); expect(token).toBeTruthy(); // Use token in subsequent requests const profileResponse = await request.get('/api/profile', { headers: { Authorization: `Bearer ${token}`, }, }); expect(profileResponse.ok()).toBeTruthy(); }); test('should mock API responses', async ({ page }) => { await page.route('/api/users', async (route) => { await route.fulfill({ status: 200, body: JSON.stringify([ { id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Smith' }, ]), }); }); await page.goto('/users'); await expect(page.getByText('John Doe')).toBeVisible(); await expect(page.getByText('Jane Smith')).toBeVisible(); }); });
5. Visual Regression Testing
Pattern: Screenshot comparison for UI changes
import { test, expect } from '@playwright/test'; test.describe('Visual Regression', () => { test('homepage matches baseline', async ({ page }) => { await page.goto('/'); await expect(page).toHaveScreenshot('homepage.png', { fullPage: true, animations: 'disabled', }); }); test('component states', async ({ page }) => { await page.goto('/components'); // Default state const button = page.getByRole('button', { name: 'Submit' }); await expect(button).toHaveScreenshot('button-default.png'); // Hover state await button.hover(); await expect(button).toHaveScreenshot('button-hover.png'); // Disabled state await page.evaluate(() => { document.querySelector('button')?.setAttribute('disabled', 'true'); }); await expect(button).toHaveScreenshot('button-disabled.png'); }); test('responsive screenshots', async ({ page }) => { await page.goto('/'); // Desktop await page.setViewportSize({ width: 1920, height: 1080 }); await expect(page).toHaveScreenshot('homepage-desktop.png'); // Tablet await page.setViewportSize({ width: 768, height: 1024 }); await expect(page).toHaveScreenshot('homepage-tablet.png'); // Mobile await page.setViewportSize({ width: 375, height: 667 }); await expect(page).toHaveScreenshot('homepage-mobile.png'); }); });
6. Mobile Emulation & Device Testing
Pattern: Test responsive behavior and touch interactions
import { test, expect, devices } from '@playwright/test'; test.use(devices['iPhone 13 Pro']); test.describe('Mobile Experience', () => { test('should render mobile navigation', async ({ page }) => { await page.goto('/'); // Mobile menu should be visible await expect(page.getByRole('button', { name: 'Menu' })).toBeVisible(); // Desktop nav should be hidden await expect(page.getByRole('navigation').first()).toBeHidden(); }); test('touch gestures', async ({ page }) => { await page.goto('/gallery'); const image = page.getByRole('img').first(); // Swipe left await image.dispatchEvent('touchstart', { touches: [{ clientX: 300, clientY: 200 }] }); await image.dispatchEvent('touchmove', { touches: [{ clientX: 100, clientY: 200 }] }); await image.dispatchEvent('touchend'); await expect(page.getByText('Next Image')).toBeVisible(); }); test('landscape orientation', async ({ page }) => { await page.setViewportSize({ width: 812, height: 375 }); // iPhone landscape await page.goto('/video'); await expect(page.locator('video')).toHaveCSS('width', '100%'); }); }); // Test across multiple devices for (const deviceName of ['iPhone 13', 'Pixel 5', 'iPad Pro']) { test.describe(`Device: ${deviceName}`, () => { test.use(devices[deviceName]); test('critical user flow', async ({ page }) => { await page.goto('/'); // Test critical flow on each device }); }); }
7. Accessibility Testing
Pattern: Automated accessibility checks
import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; test.describe('Accessibility', () => { test('should not have accessibility violations', async ({ page }) => { await page.goto('/'); const accessibilityScanResults = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) .analyze(); expect(accessibilityScanResults.violations).toEqual([]); }); test('keyboard navigation', async ({ page }) => { await page.goto('/form'); // Tab through form fields await page.keyboard.press('Tab'); await expect(page.getByLabel('Email')).toBeFocused(); await page.keyboard.press('Tab'); await expect(page.getByLabel('Password')).toBeFocused(); await page.keyboard.press('Tab'); await expect(page.getByRole('button', { name: 'Submit' })).toBeFocused(); // Submit with Enter await page.keyboard.press('Enter'); }); test('screen reader support', async ({ page }) => { await page.goto('/'); // Check ARIA labels await expect(page.getByRole('navigation', { name: 'Main' })).toBeVisible(); await expect(page.getByRole('main')).toHaveAttribute('aria-label', 'Main content'); // Check alt text const images = page.getByRole('img'); for (const img of await images.all()) { await expect(img).toHaveAttribute('alt'); } }); });
8. Performance Testing
Pattern: Monitor performance metrics
import { test, expect } from '@playwright/test'; test.describe('Performance', () => { test('page load performance', async ({ page }) => { await page.goto('/'); const performanceMetrics = await page.evaluate(() => { const perfData = window.performance.timing; return { loadTime: perfData.loadEventEnd - perfData.navigationStart, domContentLoaded: perfData.domContentLoadedEventEnd - perfData.navigationStart, firstPaint: performance.getEntriesByType('paint')[0]?.startTime || 0, }; }); expect(performanceMetrics.loadTime).toBeLessThan(3000); // 3s max expect(performanceMetrics.domContentLoaded).toBeLessThan(2000); // 2s max }); test('Core Web Vitals', async ({ page }) => { await page.goto('/'); const vitals = await page.evaluate(() => { return new Promise((resolve) => { new PerformanceObserver((list) => { const entries = list.getEntries(); const lcp = entries.find(e => e.entryType === 'largest-contentful-paint'); const fid = entries.find(e => e.entryType === 'first-input'); const cls = entries.find(e => e.entryType === 'layout-shift'); resolve({ lcp: lcp?.startTime, fid: fid?.processingStart, cls: cls?.value }); }).observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] }); }); }); expect(vitals.lcp).toBeLessThan(2500); // Good LCP expect(vitals.fid).toBeLessThan(100); // Good FID expect(vitals.cls).toBeLessThan(0.1); // Good CLS }); });
9. Advanced Configuration
playwright.config.ts:
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: [ ['html'], ['junit', { outputFile: 'test-results/junit.xml' }], ['json', { outputFile: 'test-results/results.json' }], ], use: { baseURL: process.env.BASE_URL || 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, projects: [ // Desktop browsers { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, // Mobile browsers { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] }, }, { name: 'Mobile Safari', use: { ...devices['iPhone 13'] }, }, // Tablet browsers { name: 'iPad', use: { ...devices['iPad Pro'] }, }, ], webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 120 * 1000, }, });
10. CI/CD Integration
GitHub Actions:
name: E2E Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps - name: Run E2E tests run: npm run test:e2e env: BASE_URL: https://staging.example.com - name: Upload test results if: always() uses: actions/upload-artifact@v3 with: name: playwright-report path: playwright-report/ retention-days: 30 - name: Upload traces if: failure() uses: actions/upload-artifact@v3 with: name: playwright-traces path: test-results/
11. Debugging Strategies
Tools & Techniques:
// 1. Debug mode (headed browser + slow motion) test('debug example', async ({ page }) => { await page.goto('/'); await page.pause(); // Pauses execution, opens inspector }); // 2. Console logs test('capture console', async ({ page }) => { page.on('console', msg => console.log(`Browser: ${msg.text()}`)); await page.goto('/'); }); // 3. Network inspection test('inspect network', async ({ page }) => { page.on('request', request => console.log('Request:', request.url())); page.on('response', response => console.log('Response:', response.status())); await page.goto('/'); }); // 4. Screenshots on failure test.afterEach(async ({ page }, testInfo) => { if (testInfo.status !== testInfo.expectedStatus) { await page.screenshot({ path: `screenshots/${testInfo.title}.png`, fullPage: true }); } }); // 5. Trace viewer // Run: npx playwright test --trace on // View: npx playwright show-trace trace.zip
Common Debugging Commands:
# Run in headed mode (see browser) npx playwright test --headed # Run with UI mode (interactive debugging) npx playwright test --ui # Run single test npx playwright test tests/login.spec.ts # Debug specific test npx playwright test tests/login.spec.ts --debug # Generate test code npx playwright codegen http://localhost:3000
12. Handling Flaky Tests
Patterns for Reliability:
// 1. Proper waiting strategies test('wait for content', async ({ page }) => { await page.goto('/'); // ❌ BAD: Fixed delays // await page.waitForTimeout(5000); // ✅ GOOD: Wait for specific conditions await page.waitForLoadState('networkidle'); await page.waitForSelector('.content', { state: 'visible' }); await page.getByText('Welcome').waitFor(); }); // 2. Retry logic for external dependencies test('api with retry', async ({ page }) => { await page.goto('/'); let retries = 3; while (retries > 0) { try { const response = await page.waitForResponse( response => response.url().includes('/api/data') && response.ok(), { timeout: 5000 } ); expect(response.ok()).toBeTruthy(); break; } catch (error) { retries--; if (retries === 0) throw error; await page.reload(); } } }); // 3. Test isolation test.describe.configure({ mode: 'parallel' }); test.beforeEach(async ({ page }) => { // Clear state before each test await page.context().clearCookies(); await page.context().clearPermissions(); }); // 4. Deterministic test data test('use fixtures', async ({ page }) => { // Seed database with known data await page.request.post('/api/test/seed', { data: { userId: 'test-123', email: 'test@example.com' } }); await page.goto('/users/test-123'); await expect(page.getByText('test@example.com')).toBeVisible(); // Cleanup await page.request.delete('/api/test/users/test-123'); });
Best Practices
Test Organization
e2e/ ├── fixtures/ │ ├── auth.fixture.ts │ ├── data.fixture.ts │ └── mock.fixture.ts ├── pages/ │ ├── LoginPage.ts │ ├── DashboardPage.ts │ └── ProfilePage.ts ├── tests/ │ ├── auth/ │ │ ├── login.spec.ts │ │ ├── signup.spec.ts │ │ └── logout.spec.ts │ ├── user/ │ │ ├── profile.spec.ts │ │ └── settings.spec.ts │ └── api/ │ ├── users.spec.ts │ └── posts.spec.ts └── playwright.config.ts
Naming Conventions
- Test files:
or*.spec.ts*.test.ts - Page objects:
*Page.ts - Fixtures:
*.fixture.ts - Descriptive test names:
should allow user to login with valid credentials
Performance Optimization
- Parallel execution: Run tests in parallel across workers
- Test sharding: Split tests across CI machines
- Selective testing: Use tags/annotations for smoke tests
- Reuse authentication: Save auth state, reuse across tests
- Mock external APIs: Reduce network latency and flakiness
Security Considerations
- Never commit credentials in test files
- Use environment variables for sensitive data
- Isolate test data from production
- Clear cookies/storage between tests
- Use disposable test accounts
Common Patterns
Authentication State Reuse
// global-setup.ts import { chromium, FullConfig } from '@playwright/test'; async function globalSetup(config: FullConfig) { const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto('http://localhost:3000/login'); await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').fill('password123'); await page.getByRole('button', { name: 'Login' }).click(); await page.waitForURL('http://localhost:3000/dashboard'); // Save signed-in state await page.context().storageState({ path: 'auth.json' }); await browser.close(); } export default globalSetup; // playwright.config.ts export default defineConfig({ globalSetup: require.resolve('./global-setup'), use: { storageState: 'auth.json', }, });
Multi-Tab/Window Testing
test('open in new tab', async ({ context }) => { const page = await context.newPage(); await page.goto('/'); const [newPage] = await Promise.all([ context.waitForEvent('page'), page.getByRole('link', { name: 'Open in new tab' }).click() ]); await newPage.waitForLoadState(); await expect(newPage).toHaveURL('/new-page'); });
File Upload/Download
test('upload file', async ({ page }) => { await page.goto('/upload'); const fileChooserPromise = page.waitForEvent('filechooser'); await page.getByRole('button', { name: 'Upload' }).click(); const fileChooser = await fileChooserPromise; await fileChooser.setFiles('path/to/file.pdf'); await expect(page.getByText('file.pdf uploaded')).toBeVisible(); }); test('download file', async ({ page }) => { await page.goto('/downloads'); const downloadPromise = page.waitForEvent('download'); await page.getByRole('link', { name: 'Download Report' }).click(); const download = await downloadPromise; await download.saveAs(`/tmp/${download.suggestedFilename()}`); expect(download.suggestedFilename()).toBe('report.pdf'); });
Troubleshooting
Common Issues
- Timeouts: Increase timeout, use proper wait strategies
- Flaky selectors: Use stable locators (roles, labels, test IDs)
- Race conditions: Wait for network idle, use explicit waits
- Authentication failures: Clear cookies, check auth state
- Screenshot mismatches: Update baselines, disable animations
Debug Checklist
- Test passes locally in headed mode?
- Network requests succeed (check DevTools)?
- Selectors are stable and unique?
- Proper waits before assertions?
- Test data is deterministic?
- No race conditions with async operations?
- Traces/screenshots captured on failure?
Resources
- Official Docs: https://playwright.dev
- API Reference: https://playwright.dev/docs/api/class-playwright
- Best Practices: https://playwright.dev/docs/best-practices
- Examples: https://github.com/microsoft/playwright/tree/main/examples
- Community: https://github.com/microsoft/playwright/discussions