Playwright Testing Expertise
You are an expert in Playwright testing framework with deep knowledge of browser automation, selectors, page objects, and best practices for end-to-end testing.
Your Capabilities
-
Playwright Configuration: Projects, browsers, reporters, and fixtures
-
Locators & Selectors: Role-based, text, CSS, and chained locators
-
Page Object Model: Organizing tests with page objects
-
Assertions: Built-in assertions, custom matchers, auto-waiting
-
Test Fixtures: Built-in and custom fixtures, test isolation
-
Debugging: Traces, screenshots, videos, and Playwright Inspector
-
API Testing: Request fixtures and API testing capabilities
When to Use This Skill
Claude should automatically invoke this skill when:
-
The user mentions Playwright, playwright.config, or Playwright features
-
Files matching *.spec.ts in e2e, tests, or playwright directories are encountered
-
The user asks about locators, page objects, or browser automation
-
E2E or integration testing is discussed
-
Browser testing configuration is needed
How to Use This Skill
Accessing Resources
Use {baseDir} to reference files in this skill directory:
-
Scripts: {baseDir}/scripts/
-
Documentation: {baseDir}/references/
-
Templates: {baseDir}/assets/
Available Resources
This skill includes ready-to-use resources in {baseDir} :
-
references/playwright-cheatsheet.md - Quick reference for locators, assertions, actions, and CLI commands
-
assets/page-object.template.ts - Complete Page Object Model template with base class and examples
-
scripts/check-playwright-setup.sh - Validates Playwright configuration and browser installation
Playwright Best Practices
Test Structure
import { test, expect } from '@playwright/test';
test.describe('Contact Form', () => { test.beforeEach(async ({ page }) => { await page.goto('/contact'); });
test('should show success message after form submission', async ({ page }) => { // Arrange await page.getByLabel('Name').fill('Test User'); await page.getByLabel('Email').fill('test@example.com'); await page.getByLabel('Message').fill('Hello, this is a test message.');
// Act
await page.getByRole('button', { name: 'Submit' }).click();
// Assert
await expect(page.getByText('Thank you for your message')).toBeVisible();
await expect(page.getByLabel('Name')).toBeEmpty();
}); });
Locator Best Practices
Preferred Locators (Most Resilient)
// Role-based (best) page.getByRole('button', { name: 'Submit' }); page.getByRole('textbox', { name: 'Email' }); page.getByRole('heading', { level: 1 });
// Label-based page.getByLabel('Email address'); page.getByPlaceholder('Enter your email');
// Text-based page.getByText('Welcome'); page.getByTitle('Close');
Chaining Locators
page.getByRole('listitem') .filter({ hasText: 'Product 1' }) .getByRole('button', { name: 'Add' });
Test IDs (Last Resort)
page.getByTestId('submit-button');
Page Object Pattern
// pages/login.page.ts import { Page, Locator, expect } from '@playwright/test';
export class LoginPage { private readonly page: Page; readonly emailInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; readonly errorMessage: Locator;
constructor(page: Page) { this.page = page; this.emailInput = page.getByLabel('Email'); this.passwordInput = page.getByLabel('Password'); this.submitButton = page.getByRole('button', { name: 'Sign in' }); this.errorMessage = page.getByRole('alert'); }
async goto() { await this.page.goto('/login'); await expect(this.emailInput).toBeVisible(); }
async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.submitButton.click(); }
async getError(): Promise<string | null> { if (await this.errorMessage.isVisible()) { return this.errorMessage.textContent(); } return null; } }
// Usage in test import { test, expect } from '@playwright/test'; import { LoginPage } from './pages/login.page';
test('should login successfully', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('user@test.com', 'password'); await expect(page).toHaveURL('/dashboard'); });
test('should show error for invalid credentials', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('invalid@test.com', 'wrongpassword'); const error = await loginPage.getError(); expect(error).toContain('Invalid credentials'); });
Auto-Waiting & Assertions
// Auto-waits for element await expect(page.getByRole('alert')).toBeVisible(); await expect(page.getByRole('button')).toBeEnabled(); await expect(page.getByText('Count: 5')).toBeVisible();
// Negative assertions await expect(page.getByRole('dialog')).toBeHidden(); await expect(page.getByText('Error')).not.toBeVisible();
// With custom timeout await expect(page.getByText('Loaded')).toBeVisible({ timeout: 10000 });
Fixtures
// fixtures.ts import { test as base } from '@playwright/test';
export const test = base.extend<{ authenticatedPage: Page; }>({ authenticatedPage: async ({ page }, use) => { await page.goto('/login'); await page.getByLabel('Email').fill('test@test.com'); await page.getByLabel('Password').fill('password'); await page.getByRole('button', { name: 'Login' }).click(); await page.waitForURL('/dashboard'); await use(page); }, });
Storage State Authentication
For efficient authentication without UI login each time:
// Setup: Save auth state after login (run once) // auth.setup.ts import { test as setup, expect } from '@playwright/test';
setup('authenticate', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('test@example.com'); await page.getByLabel('Password').fill('password'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page).toHaveURL('/dashboard');
// Save storage state (cookies, localStorage) await page.context().storageState({ path: '.auth/user.json' }); });
// playwright.config.ts export default defineConfig({ projects: [ { name: 'setup', testMatch: /.*.setup.ts/ }, { name: 'chromium', use: { storageState: '.auth/user.json' }, dependencies: ['setup'], }, ], });
// Tests automatically have auth state test('dashboard loads for authenticated user', async ({ page }) => { await page.goto('/dashboard'); await expect(page.getByText('Welcome back')).toBeVisible(); });
Network Mocking & Interception
Mock API responses for reliable, fast tests:
import { test, expect } from '@playwright/test';
test('should display mocked user data', async ({ page }) => { // Mock API response await page.route('**/api/users', route => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { id: 1, name: 'Test User', email: 'test@example.com' } ]), }); });
await page.goto('/users'); await expect(page.getByText('Test User')).toBeVisible(); });
test('should handle API errors gracefully', async ({ page }) => { // Mock error response await page.route('**/api/users', route => { route.fulfill({ status: 500, body: JSON.stringify({ error: 'Internal Server Error' }), }); });
await page.goto('/users'); await expect(page.getByText('Failed to load users')).toBeVisible(); });
test('should handle network failure', async ({ page }) => { // Abort network request await page.route('**/api/data', route => route.abort());
await page.goto('/data'); await expect(page.getByText('Network error')).toBeVisible(); });
test('should handle slow responses', async ({ page }) => { // Simulate slow API await page.route('**/api/slow', async route => { await new Promise(resolve => setTimeout(resolve, 3000)); await route.continue(); });
await page.goto('/slow-page'); await expect(page.getByText('Loading...')).toBeVisible(); });
// Modify request/response test('should modify request headers', async ({ page }) => { await page.route('/api/', route => { route.continue({ headers: { ...route.request().headers(), 'X-Test-Header': 'test-value', }, }); }); });
Accessibility Testing
Integrate accessibility audits with @axe-core/playwright:
// Install: npm install @axe-core/playwright import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright';
test('should pass accessibility audit', async ({ page }) => { await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]); });
test('should pass accessibility audit for specific section', async ({ page }) => { await page.goto('/dashboard');
const results = await new AxeBuilder({ page }) .include('#main-content') .exclude('#third-party-widget') .withTags(['wcag2a', 'wcag2aa']) .analyze();
expect(results.violations).toEqual([]); });
// Check specific rules test('should have proper color contrast', async ({ page }) => { await page.goto('/');
const results = await new AxeBuilder({ page }) .withRules(['color-contrast']) .analyze();
expect(results.violations).toEqual([]); });
// Detailed violation reporting test('accessibility check with detailed report', async ({ page }) => { await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
if (results.violations.length > 0) {
console.log('Accessibility violations:');
results.violations.forEach(violation => {
console.log(- ${violation.id}: ${violation.description});
violation.nodes.forEach(node => {
console.log( Element: ${node.html});
console.log( Fix: ${node.failureSummary});
});
});
}
expect(results.violations).toEqual([]); });
Visual Regression Testing
Compare screenshots to detect visual changes:
import { test, expect } from '@playwright/test';
test('homepage visual regression', async ({ page }) => { await page.goto('/');
// Full page screenshot comparison await expect(page).toHaveScreenshot('homepage.png'); });
test('component visual regression', async ({ page }) => { await page.goto('/components');
// Element-specific screenshot const button = page.getByRole('button', { name: 'Submit' }); await expect(button).toHaveScreenshot('submit-button.png'); });
test('visual with threshold', async ({ page }) => { await page.goto('/');
// Allow small differences await expect(page).toHaveScreenshot('homepage.png', { maxDiffPixels: 100, threshold: 0.2, }); });
// Update snapshots: npx playwright test --update-snapshots
Playwright Configuration
Basic 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', use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-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 13'] } }, ], webServer: { command: 'npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, });
Debugging & Troubleshooting
Debug Mode
npx playwright test --debug npx playwright test --ui
Traces
// Capture trace on failure use: { trace: 'on-first-retry', }
// View trace npx playwright show-trace trace.zip
Screenshots
await page.screenshot({ path: 'screenshot.png', fullPage: true });
Common Issues & Solutions
Issue: Flaky tests
-
Use auto-waiting assertions instead of fixed waits
-
Wait for specific elements instead of networkidle (which fails with WebSockets, long-polling, analytics): // Bad: networkidle is unreliable await page.waitForLoadState('networkidle');
// Good: wait for specific content await expect(page.getByRole('main')).toBeVisible(); await expect(page.getByTestId('data-loaded')).toBeAttached();
- Ensure proper test isolation
Issue: Elements not found
-
Use Playwright Inspector to find better locators
-
Prefer role-based selectors
-
Check for iframes or shadow DOM
Issue: Slow tests
-
Reuse authentication state
-
Use test.describe.parallel()
-
Mock slow API calls
Examples
Example 1: Form Testing
When testing forms:
-
Use getByLabel for inputs
-
Use fill() instead of type() for speed
-
Submit with button locator
-
Assert on success message or navigation
Example 2: Table Testing
When testing tables/lists:
-
Use getByRole('row') for table rows
-
Filter by content with .filter()
-
Chain to find actions within rows
-
Assert on row count or content
Version Compatibility
The patterns in this skill require the following minimum versions:
Feature Minimum Version Notes
getByRole with name 1.27+ Role-based locators with accessible name
toHaveScreenshot 1.22+ Visual regression testing
storageState 1.13+ Authentication state persistence
@axe-core/playwright 4.7+ Accessibility testing integration
route.fulfill 1.0+ Network mocking (stable)
test.describe.configure 1.24+ Parallel/serial test configuration
Feature Detection
Check your Playwright version:
npx playwright --version
Upgrading
Update Playwright
npm install -D @playwright/test@latest
Update browsers
npx playwright install
Important Notes
-
Playwright is automatically invoked when relevant
-
Always check playwright.config.ts for project settings
-
Prefer role-based locators for resilient tests
-
Use auto-waiting assertions instead of explicit waits
-
Consider mobile and cross-browser testing in CI