Playwright Core Knowledge
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: playwright for comprehensive documentation.
When NOT to Use This Skill
- Unit Testing - Use
vitest or jest for isolated function/module tests - Component Testing - Use
testing-library for React/Vue component tests - API-Only Tests - Use framework-specific HTTP clients or REST clients
- Performance Testing - Use dedicated tools like k6 or Lighthouse
- Mobile App Testing - Use Appium or Detox for native mobile apps
Basic Test
import { test, expect } from '@playwright/test';
test('user can login', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Welcome');
});
Locators
// By role (preferred)
page.getByRole('button', { name: 'Submit' });
page.getByRole('link', { name: 'Home' });
page.getByLabel('Email');
page.getByPlaceholder('Enter email');
page.getByText('Welcome');
// By test id
page.getByTestId('submit-button');
// CSS/XPath (fallback)
page.locator('.submit-btn');
page.locator('#user-form');
Assertions
// Visibility
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
// Text
await expect(locator).toHaveText('Hello');
await expect(locator).toContainText('Hello');
// Attributes
await expect(locator).toHaveAttribute('href', '/home');
await expect(locator).toHaveClass(/active/);
// Input
await expect(locator).toHaveValue('test@example.com');
// Page
await expect(page).toHaveURL(/dashboard/);
await expect(page).toHaveTitle('Dashboard');
Actions
await page.click('button');
await page.fill('input', 'text');
await page.check('input[type="checkbox"]');
await page.selectOption('select', 'option1');
await page.hover('.menu-item');
await page.keyboard.press('Enter');
Page Objects
class LoginPage {
constructor(private page: Page) {}
async login(email: string, password: string) {
await this.page.fill('[name="email"]', email);
await this.page.fill('[name="password"]', password);
await this.page.click('button[type="submit"]');
}
}
Config
// playwright.config.ts
export default defineConfig({
testDir: './e2e',
use: { baseURL: 'http://localhost:3000' },
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }
]
});
Production Readiness
Test 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' }],
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
},
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,
timeout: 120000,
},
});
Authentication State
// Save auth state for reuse
// auth.setup.ts
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', process.env.TEST_USER_EMAIL!);
await page.fill('[name="password"]', process.env.TEST_USER_PASSWORD!);
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
// Save storage state
await page.context().storageState({ path: '.auth/user.json' });
});
// Use in tests
import { test } from '@playwright/test';
test.use({ storageState: '.auth/user.json' });
test('authenticated test', async ({ page }) => {
await page.goto('/dashboard');
// Already logged in
});
API Testing
test('API test', async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'John', email: 'john@example.com' },
headers: { Authorization: `Bearer ${token}` },
});
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user.name).toBe('John');
});
// Mock API responses
test('with mocked API', async ({ page }) => {
await page.route('/api/users', async route => {
await route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'Mock User' }]),
});
});
await page.goto('/users');
await expect(page.getByText('Mock User')).toBeVisible();
});
Visual Regression
test('visual regression', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
maxDiffPixels: 100,
threshold: 0.2,
});
});
// Update snapshots: npx playwright test --update-snapshots
CI Configuration
# GitHub Actions
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
env:
BASE_URL: ${{ secrets.STAGING_URL }}
- name: Upload report
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
Monitoring Metrics
| Metric | Target |
|---|
| E2E test pass rate | > 99% |
| Test execution time | < 10min |
| Flaky test rate | < 1% |
| Visual diff failures | Review all |
Checklist
- Multi-browser testing configured
- Mobile viewport testing
- Authentication state reused
- Retry on failure (CI only)
- Screenshots on failure
- Trace collection enabled
- API mocking for isolation
- Visual regression tests
- CI/CD integration
- Test parallelization
- Page Object pattern used
Anti-Patterns
| Anti-Pattern | Why It's Bad | Solution |
|---|
| Testing via CSS selectors | Brittle, breaks on style changes | Use getByRole, getByLabel, getByTestId |
| Not waiting for elements | Flaky tests | Use auto-waiting locators, avoid waitForTimeout |
| Hardcoded waits (page.waitForTimeout) | Slow, unreliable | Use page.waitForSelector or auto-waiting |
| No Page Object Model | Duplicated code, hard to maintain | Extract common actions into Page Objects |
| Testing too much in one test | Hard to debug failures | One user flow per test |
| Not reusing auth state | Slow login for every test | Save storageState, reuse across tests |
| Ignoring flaky tests | False confidence | Fix flaky tests, use retries sparingly |
Quick Troubleshooting
| Problem | Likely Cause | Solution |
|---|
| "Timeout waiting for selector" | Element not rendered or wrong selector | Check selector, ensure element exists |
| Flaky test (passes/fails randomly) | Race condition, slow network | Use auto-waiting, avoid waitForTimeout |
| "Element is not visible" | Element hidden or not in viewport | Scroll into view, check CSS display |
| "Execution context destroyed" | Navigation happened during action | Wait for navigation to complete |
| Screenshot mismatch | Font rendering, animation | Disable animations, use fixed viewport |
| Test hangs forever | Missing await | Ensure all async calls are awaited |
Reference Documentation