Files
gh-cskiro-claudex-testing-t…/skills/playwright-e2e-automation/data/playwright-best-practices.md
2025-11-29 18:17:04 +08:00

457 lines
11 KiB
Markdown

# Playwright Best Practices
Official best practices for Playwright test automation, optimized for LLM-assisted development.
## Test Structure
### Use Page Object Models (POM)
**Why**: Separates page structure from test logic, improves maintainability
```typescript
// Good: Page Object Model
// pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(username: string, password: string) {
await this.page.getByLabel('Username').fill(username);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Sign in' }).click();
}
}
// specs/login.spec.ts
test('user can login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
```
### Use Semantic Selectors
**Priority order** (most stable → least stable):
1. **getByRole** - Accessible role (button, heading, textbox, etc.)
2. **getByLabel** - Form inputs with associated labels
3. **getByPlaceholder** - Input placeholder text
4. **getByText** - User-visible text content
5. **getByTestId** - data-testid attributes (last resort)
```typescript
// Best: Role-based (accessible and stable)
await page.getByRole('button', { name: 'Submit' }).click();
// Good: Label-based (for forms)
await page.getByLabel('Email address').fill('user@example.com');
// Acceptable: Text-based
await page.getByText('Continue to checkout').click();
// Avoid: CSS selectors (brittle)
await page.click('.btn-primary'); // ❌ Breaks if class changes
// Last resort: Test IDs (when semantic selectors don't work)
await page.getByTestId('checkout-button').click();
```
## Screenshot Best Practices
### When to Capture Screenshots
1. **Initial page load** - Baseline visual state
2. **Before interaction** - Pre-state for comparison
3. **After interaction** - Result of user action
4. **Error states** - When validation fails or errors occur
5. **Success states** - Confirmation screens, success messages
6. **Test failures** - Automatic capture for debugging
### Screenshot Naming Convention
```typescript
// Pattern: {test-name}-{viewport}-{state}-{timestamp}.png
await page.screenshot({
path: `screenshots/current/login-desktop-initial-${Date.now()}.png`,
fullPage: true
});
await page.screenshot({
path: `screenshots/current/checkout-mobile-error-${Date.now()}.png`,
fullPage: true
});
```
### Full-Page vs Element Screenshots
```typescript
// Full-page: For layout and overall UI analysis
await page.screenshot({
path: 'homepage-full.png',
fullPage: true // Captures entire scrollable page
});
// Element-specific: For component testing
const button = page.getByRole('button', { name: 'Submit' });
await button.screenshot({
path: 'submit-button.png'
});
```
## Waiting and Timing
### Auto-Waiting
Playwright automatically waits for:
- Element to be attached to DOM
- Element to be visible
- Element to be stable (not animating)
- Element to receive events (not obscured)
- Element to be enabled
```typescript
// This automatically waits for button to be clickable
await page.getByRole('button', { name: 'Submit' }).click();
```
### Explicit Waits (when needed)
```typescript
// Wait for navigation
await page.waitForURL('/dashboard');
// Wait for network idle (good before screenshots)
await page.waitForLoadState('networkidle');
// Wait for specific element
await page.waitForSelector('img[alt="Profile picture"]');
// Wait for custom condition
await page.waitForFunction(() => window.scrollY === 0);
```
### Avoid Fixed Timeouts
```typescript
// Bad: Arbitrary delays
await page.waitForTimeout(3000); // ❌ Flaky, slow
// Good: Wait for specific condition
await expect(page.getByText('Success')).toBeVisible(); // ✅ Fast and reliable
```
## Test Isolation
### Independent Tests
Each test should be completely independent:
```typescript
// Good: Test is self-contained
test('user can add item to cart', async ({ page }) => {
// Set up: Create user, log in
await page.goto('/');
await login(page, 'user@example.com', 'password');
// Action: Add to cart
await page.getByRole('button', { name: 'Add to cart' }).click();
// Assert: Item in cart
await expect(page.getByTestId('cart-count')).toHaveText('1');
// Cleanup happens automatically with new page context
});
// Bad: Depends on previous test state
test('user can checkout', async ({ page }) => {
// ❌ Assumes cart already has items from previous test
await page.goto('/checkout');
// ...
});
```
### Use test.beforeEach for Common Setup
```typescript
test.describe('Shopping cart', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await login(page, 'user@example.com', 'password');
});
test('can add item to cart', async ({ page }) => {
// Setup already done
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
});
test('can remove item from cart', async ({ page }) => {
// Setup already done, fresh state
await page.getByRole('button', { name: 'Add to cart' }).click();
await page.getByRole('button', { name: 'Remove' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('0');
});
});
```
## Visual Regression Testing
### Snapshot Testing
```typescript
// Basic snapshot
await expect(page).toHaveScreenshot('homepage.png');
// With threshold (allow minor differences)
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixelRatio: 0.05 // Allow 5% difference
});
// Element snapshot
const card = page.getByRole('article').first();
await expect(card).toHaveScreenshot('product-card.png');
```
### Updating Baselines
```bash
# Update all snapshots
npx playwright test --update-snapshots
# Update specific test
npx playwright test login.spec.ts --update-snapshots
```
### Baseline Management
- **Store baselines in git** - Commit to repository for consistency
- **Review diffs carefully** - Not all changes are bugs
- **Update deliberately** - Only update when changes are intentional
- **Use CI checks** - Fail pipeline on unexpected visual changes
## Configuration Best Practices
### playwright.config.ts Essentials
```typescript
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// Timeout for each test
timeout: 30 * 1000,
// Global setup/teardown
globalSetup: require.resolve('./tests/setup/global-setup.ts'),
// Fail fast on CI, retry locally
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
// Parallel execution
workers: process.env.CI ? 1 : undefined,
// Reporter
reporter: process.env.CI ? 'github' : 'html',
use: {
// Base URL
baseURL: 'http://localhost:5173',
// Screenshot on failure
screenshot: 'only-on-failure',
// Trace on first retry
trace: 'on-first-retry',
// Video on failure
video: 'retain-on-failure',
},
// Projects for multi-browser testing
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 13'] },
},
],
// Web server for dev
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});
```
## Debugging
### Playwright Inspector
```bash
# Debug specific test
npx playwright test --debug login.spec.ts
# Debug from specific line
npx playwright test --debug --grep "user can login"
```
### VS Code Debugger
```json
// .vscode/launch.json
{
"configurations": [
{
"name": "Debug Playwright Tests",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/@playwright/test/cli.js",
"args": ["test", "--headed", "${file}"],
"console": "integratedTerminal"
}
]
}
```
### Trace Viewer
```bash
# Run with trace
npx playwright test --trace on
# View trace
npx playwright show-trace trace.zip
```
## Performance Optimization
### Parallel Execution
```typescript
// Run tests in parallel (default)
test.describe.configure({ mode: 'parallel' });
// Run tests serially (when needed)
test.describe.configure({ mode: 'serial' });
```
### Reuse Authentication State
```typescript
// global-setup.ts
import { chromium } from '@playwright/test';
export default async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('http://localhost:5173/login');
await page.getByLabel('Username').fill('admin');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
// Save authentication state
await page.context().storageState({ path: 'auth.json' });
await browser.close();
}
// Use in tests
test.use({ storageState: 'auth.json' });
```
## Common Pitfalls to Avoid
### 1. Not Waiting for Network Idle Before Screenshots
```typescript
// Bad: Screenshot may capture loading state
await page.goto('/dashboard');
await page.screenshot({ path: 'dashboard.png' });
// Good: Wait for content to load
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'dashboard.png' });
```
### 2. Using Non-Stable Selectors
```typescript
// Bad: Position-based (breaks if order changes)
await page.locator('button').nth(2).click();
// Good: Content-based
await page.getByRole('button', { name: 'Submit' }).click();
```
### 3. Not Handling Dynamic Content
```typescript
// Bad: Assumes content is already loaded
const text = await page.getByTestId('user-name').textContent();
// Good: Wait for element first
await expect(page.getByTestId('user-name')).toBeVisible();
const text = await page.getByTestId('user-name').textContent();
```
### 4. Overly Broad Assertions
```typescript
// Bad: Fails on any minor change
await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0 });
// Good: Allow reasonable tolerance
await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0.02 });
```
## Summary Checklist
- [ ] Use Page Object Models for test organization
- [ ] Prefer semantic selectors (getByRole, getByLabel)
- [ ] Capture screenshots at key interaction points
- [ ] Wait for network idle before screenshots
- [ ] Use auto-waiting instead of fixed timeouts
- [ ] Make tests independent and isolated
- [ ] Configure proper retry logic (2-3 retries in CI)
- [ ] Store authentication state for reuse
- [ ] Use trace viewer for debugging
- [ ] Review visual diffs before updating baselines
- [ ] Run tests in parallel for performance
- [ ] Enable screenshot/video on failure
- [ ] Store baselines in version control
- [ ] Use meaningful screenshot names with timestamps
- [ ] Configure appropriate visual diff thresholds
---
**References:**
- [Playwright Official Docs](https://playwright.dev/)
- [Best Practices Guide](https://playwright.dev/docs/best-practices)
- [Locators Guide](https://playwright.dev/docs/locators)