Files
2025-11-29 17:57:09 +08:00

1082 lines
28 KiB
Markdown

# /specweave-testing:e2e-setup
Set up comprehensive Playwright E2E testing with best practices, page objects, and CI/CD integration.
You are an expert E2E testing engineer who implements production-ready Playwright test suites.
## Your Task
Set up a complete Playwright E2E testing framework with page objects, fixtures, and testing patterns.
### 1. Playwright Stack Features
**Cross-Browser Testing**:
- Chromium, Firefox, WebKit support
- Mobile viewport emulation
- Device-specific testing (iPhone, Pixel, etc.)
- Browser context isolation
- Persistent state management
**Reliability Features**:
- Auto-wait for elements
- Network idle detection
- Retry mechanisms
- Screenshot/video on failure
- Trace recording for debugging
**Performance**:
- Parallel test execution
- Sharding for CI/CD
- Browser reuse
- Worker threads
- Test isolation
### 2. Advanced Playwright Configuration
**playwright.config.ts** (Production-Grade):
```typescript
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';
dotenv.config();
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
// Reporter configuration
reporter: [
['html', { outputFolder: 'playwright-report' }],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }],
['github'], // GitHub Actions annotations
],
use: {
// Base URL for navigation
baseURL: process.env.BASE_URL || 'http://localhost:3000',
// Tracing and debugging
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
// Timeouts
actionTimeout: 10000,
navigationTimeout: 30000,
// Browser options
viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true,
// Collect HTTP Archive (HAR) files
recordHar: process.env.CI ? undefined : { path: 'test-results/har' },
},
// Test timeout
timeout: 30000,
// Global setup/teardown
globalSetup: require.resolve('./tests/e2e/global-setup.ts'),
globalTeardown: require.resolve('./tests/e2e/global-teardown.ts'),
projects: [
// Setup project for authentication
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// Desktop browsers
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
// Mobile browsers
{
name: 'mobile-chrome',
use: {
...devices['Pixel 5'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
{
name: 'mobile-safari',
use: {
...devices['iPhone 12'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
// Branded browsers
{
name: 'edge',
use: {
...devices['Desktop Edge'],
channel: 'msedge',
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
{
name: 'chrome',
use: {
...devices['Desktop Chrome'],
channel: 'chrome',
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
// Web server configuration
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000,
stdout: 'pipe',
stderr: 'pipe',
},
});
```
### 3. Page Object Model (POM)
**tests/e2e/pages/BasePage.ts**:
```typescript
import { Page, Locator } from '@playwright/test';
export abstract class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
// Common navigation
async goto(path: string) {
await this.page.goto(path);
}
// Wait helpers
async waitForNetworkIdle() {
await this.page.waitForLoadState('networkidle');
}
async waitForDomContentLoaded() {
await this.page.waitForLoadState('domcontentloaded');
}
// Screenshot helpers
async takeScreenshot(name: string) {
await this.page.screenshot({
path: `test-results/screenshots/${name}.png`,
fullPage: true,
});
}
// Cookie helpers
async getCookies() {
return await this.page.context().cookies();
}
async setCookies(cookies: any[]) {
await this.page.context().addCookies(cookies);
}
// Local storage helpers
async getLocalStorage(key: string): Promise<string | null> {
return await this.page.evaluate((key) => {
return localStorage.getItem(key);
}, key);
}
async setLocalStorage(key: string, value: string) {
await this.page.evaluate(({ key, value }) => {
localStorage.setItem(key, value);
}, { key, value });
}
// Common assertions
async assertUrl(expectedUrl: string) {
await this.page.waitForURL(expectedUrl);
}
async assertTitle(expectedTitle: string) {
await this.page.waitForFunction(
(title) => document.title === title,
expectedTitle
);
}
}
```
**tests/e2e/pages/LoginPage.ts**:
```typescript
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
// Locators
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
readonly rememberMeCheckbox: Locator;
readonly forgotPasswordLink: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.locator('input[name="email"]');
this.passwordInput = page.locator('input[name="password"]');
this.loginButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('[role="alert"]');
this.rememberMeCheckbox = page.locator('input[name="rememberMe"]');
this.forgotPasswordLink = page.locator('a[href="/forgot-password"]');
}
// Actions
async navigate() {
await this.goto('/login');
}
async login(email: string, password: string, rememberMe = false) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
if (rememberMe) {
await this.rememberMeCheckbox.check();
}
await this.loginButton.click();
}
async loginAsAdmin() {
await this.login(
process.env.ADMIN_EMAIL || 'admin@example.com',
process.env.ADMIN_PASSWORD || 'admin123'
);
}
async loginAsUser() {
await this.login(
process.env.USER_EMAIL || 'user@example.com',
process.env.USER_PASSWORD || 'user123'
);
}
// Assertions
async assertErrorMessage(expectedMessage: string) {
await expect(this.errorMessage).toContainText(expectedMessage);
}
async assertLoginSuccessful() {
await this.page.waitForURL(/\/(dashboard|home)/);
}
async assertOnLoginPage() {
await expect(this.page).toHaveURL(/\/login/);
}
}
```
**tests/e2e/pages/DashboardPage.ts**:
```typescript
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class DashboardPage extends BasePage {
readonly welcomeMessage: Locator;
readonly userMenu: Locator;
readonly logoutButton: Locator;
readonly notificationBadge: Locator;
readonly searchInput: Locator;
constructor(page: Page) {
super(page);
this.welcomeMessage = page.locator('h1');
this.userMenu = page.locator('[data-testid="user-menu"]');
this.logoutButton = page.locator('button:has-text("Logout")');
this.notificationBadge = page.locator('[data-testid="notification-badge"]');
this.searchInput = page.locator('input[placeholder="Search..."]');
}
async navigate() {
await this.goto('/dashboard');
}
async logout() {
await this.userMenu.click();
await this.logoutButton.click();
}
async search(query: string) {
await this.searchInput.fill(query);
await this.searchInput.press('Enter');
}
async getNotificationCount(): Promise<number> {
const text = await this.notificationBadge.textContent();
return parseInt(text || '0', 10);
}
async assertWelcomeMessage(username: string) {
await expect(this.welcomeMessage).toContainText(`Welcome, ${username}`);
}
async assertOnDashboard() {
await expect(this.page).toHaveURL(/\/dashboard/);
}
}
```
### 4. Custom Fixtures
**tests/e2e/fixtures/auth.fixture.ts**:
```typescript
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
type AuthFixtures = {
authenticatedPage: Page;
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.loginAsUser();
await use(page);
},
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
});
export { expect } from '@playwright/test';
```
**tests/e2e/fixtures/api.fixture.ts**:
```typescript
import { test as base, APIRequestContext } from '@playwright/test';
type ApiFixtures = {
apiContext: APIRequestContext;
};
export const test = base.extend<ApiFixtures>({
apiContext: async ({ playwright }, use) => {
const context = await playwright.request.newContext({
baseURL: process.env.API_BASE_URL || 'http://localhost:3000/api',
extraHTTPHeaders: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
await use(context);
await context.dispose();
},
});
export { expect } from '@playwright/test';
```
### 5. Global Setup and Teardown
**tests/e2e/global-setup.ts**:
```typescript
import { chromium, FullConfig } from '@playwright/test';
import path from 'path';
import fs from 'fs';
async function globalSetup(config: FullConfig) {
const { baseURL, storageState } = config.projects[0].use;
// Create auth directory
const authDir = path.dirname(storageState as string);
if (!fs.existsSync(authDir)) {
fs.mkdirSync(authDir, { recursive: true });
}
// Launch browser and authenticate
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
try {
// Navigate to login
await page.goto(`${baseURL}/login`);
// Perform login
await page.fill('input[name="email"]', process.env.USER_EMAIL || 'test@example.com');
await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password123');
await page.click('button[type="submit"]');
// Wait for successful login
await page.waitForURL(/\/(dashboard|home)/);
// Save authentication state
await context.storageState({ path: storageState as string });
console.log('✓ Global setup: Authentication completed');
} catch (error) {
console.error('✗ Global setup: Authentication failed', error);
throw error;
} finally {
await browser.close();
}
}
export default globalSetup;
```
**tests/e2e/global-teardown.ts**:
```typescript
import { FullConfig } from '@playwright/test';
import fs from 'fs';
import path from 'path';
async function globalTeardown(config: FullConfig) {
try {
// Clean up auth state
const authDir = 'playwright/.auth';
if (fs.existsSync(authDir)) {
fs.rmSync(authDir, { recursive: true });
console.log('✓ Global teardown: Cleaned auth state');
}
// Clean up test artifacts if not in CI
if (!process.env.CI) {
const artifactDirs = ['test-results', 'playwright-report'];
artifactDirs.forEach(dir => {
if (fs.existsSync(dir)) {
const files = fs.readdirSync(dir);
console.log(`✓ Global teardown: ${dir} contains ${files.length} files`);
}
});
}
} catch (error) {
console.error('✗ Global teardown: Cleanup failed', error);
}
}
export default globalTeardown;
```
### 6. Authentication Setup Tests
**tests/e2e/auth.setup.ts**:
```typescript
import { test as setup, expect } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '../../playwright/.auth/user.json');
setup('authenticate', async ({ page }) => {
// Navigate to login page
await page.goto('/login');
// Perform login
await page.fill('input[name="email"]', process.env.USER_EMAIL || 'test@example.com');
await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password123');
await page.click('button[type="submit"]');
// Wait for successful navigation
await page.waitForURL(/\/(dashboard|home)/);
// Verify authentication succeeded
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
// Save signed-in state
await page.context().storageState({ path: authFile });
});
setup('admin authenticate', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', process.env.ADMIN_EMAIL || 'admin@example.com');
await page.fill('input[name="password"]', process.env.ADMIN_PASSWORD || 'admin123');
await page.click('button[type="submit"]');
await page.waitForURL(/\/(dashboard|admin)/);
await expect(page.locator('[data-testid="admin-menu"]')).toBeVisible();
await page.context().storageState({
path: path.join(__dirname, '../../playwright/.auth/admin.json')
});
});
```
### 7. Test Utilities
**tests/e2e/utils/test-helpers.ts**:
```typescript
import { Page, expect } from '@playwright/test';
export class TestHelpers {
static async waitForApiResponse(page: Page, urlPattern: string | RegExp, timeout = 5000) {
return await page.waitForResponse(
response => {
const url = response.url();
const matches = typeof urlPattern === 'string'
? url.includes(urlPattern)
: urlPattern.test(url);
return matches && response.status() === 200;
},
{ timeout }
);
}
static async interceptApi(page: Page, urlPattern: string | RegExp, mockResponse: any) {
await page.route(urlPattern, route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockResponse),
});
});
}
static async blockRequests(page: Page, resourceTypes: string[]) {
await page.route('**/*', route => {
if (resourceTypes.includes(route.request().resourceType())) {
route.abort();
} else {
route.continue();
}
});
}
static async mockGeolocation(page: Page, latitude: number, longitude: number) {
await page.context().setGeolocation({ latitude, longitude });
await page.context().grantPermissions(['geolocation']);
}
static async fillForm(page: Page, formData: Record<string, string>) {
for (const [name, value] of Object.entries(formData)) {
await page.fill(`[name="${name}"]`, value);
}
}
static async selectDropdown(page: Page, selector: string, value: string) {
await page.selectOption(selector, value);
}
static async uploadFile(page: Page, selector: string, filePath: string) {
await page.setInputFiles(selector, filePath);
}
static async scrollToElement(page: Page, selector: string) {
await page.locator(selector).scrollIntoViewIfNeeded();
}
static async assertAccessibility(page: Page) {
// Basic accessibility checks
const violations = await page.evaluate(() => {
const issues: string[] = [];
// Check for alt text on images
document.querySelectorAll('img').forEach(img => {
if (!img.alt) {
issues.push(`Image missing alt text: ${img.src}`);
}
});
// Check for form labels
document.querySelectorAll('input, textarea, select').forEach(input => {
const id = input.getAttribute('id');
if (id && !document.querySelector(`label[for="${id}"]`)) {
issues.push(`Form element missing label: ${id}`);
}
});
return issues;
});
expect(violations).toHaveLength(0);
}
}
```
**tests/e2e/utils/mock-data.ts**:
```typescript
export const mockUsers = [
{
id: '1',
email: 'john@example.com',
name: 'John Doe',
role: 'user',
},
{
id: '2',
email: 'admin@example.com',
name: 'Admin User',
role: 'admin',
},
];
export const mockProducts = [
{
id: '1',
name: 'Product A',
price: 29.99,
inStock: true,
},
{
id: '2',
name: 'Product B',
price: 49.99,
inStock: false,
},
];
export function createMockApiResponse(data: any, delay = 0) {
return {
body: JSON.stringify({ data, success: true }),
status: 200,
headers: { 'Content-Type': 'application/json' },
delay,
};
}
```
### 8. Example E2E Tests
**tests/e2e/auth/login.spec.ts**:
```typescript
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
test.describe('Login Flow', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
await loginPage.navigate();
});
test('should login with valid credentials', async ({ page }) => {
await loginPage.login('test@example.com', 'password123');
await dashboardPage.assertOnDashboard();
await dashboardPage.assertWelcomeMessage('Test User');
});
test('should show error for invalid credentials', async () => {
await loginPage.login('wrong@example.com', 'wrongpassword');
await loginPage.assertErrorMessage('Invalid credentials');
await loginPage.assertOnLoginPage();
});
test('should validate empty fields', async () => {
await loginPage.loginButton.click();
await expect(loginPage.emailInput).toHaveAttribute('aria-invalid', 'true');
await expect(loginPage.passwordInput).toHaveAttribute('aria-invalid', 'true');
});
test('should remember user when checkbox checked', async ({ page }) => {
await loginPage.login('test@example.com', 'password123', true);
await dashboardPage.assertOnDashboard();
const cookies = await page.context().cookies();
const rememberMeCookie = cookies.find(c => c.name === 'rememberMe');
expect(rememberMeCookie).toBeDefined();
});
test('should navigate to forgot password', async ({ page }) => {
await loginPage.forgotPasswordLink.click();
await expect(page).toHaveURL('/forgot-password');
});
});
```
**tests/e2e/api/users.spec.ts**:
```typescript
import { test, expect } from '../fixtures/api.fixture';
test.describe('User API', () => {
test('should fetch users list', async ({ apiContext }) => {
const response = await apiContext.get('/users');
expect(response.ok()).toBeTruthy();
const data = await response.json();
expect(data).toHaveProperty('users');
expect(Array.isArray(data.users)).toBeTruthy();
});
test('should create new user', async ({ apiContext }) => {
const newUser = {
email: 'newuser@example.com',
name: 'New User',
role: 'user',
};
const response = await apiContext.post('/users', { data: newUser });
expect(response.ok()).toBeTruthy();
const data = await response.json();
expect(data.user).toMatchObject(newUser);
expect(data.user.id).toBeDefined();
});
test('should update user', async ({ apiContext }) => {
const updates = { name: 'Updated Name' };
const response = await apiContext.patch('/users/1', { data: updates });
expect(response.ok()).toBeTruthy();
const data = await response.json();
expect(data.user.name).toBe(updates.name);
});
test('should delete user', async ({ apiContext }) => {
const response = await apiContext.delete('/users/1');
expect(response.ok()).toBeTruthy();
// Verify deletion
const getResponse = await apiContext.get('/users/1');
expect(getResponse.status()).toBe(404);
});
test('should handle authentication errors', async ({ apiContext }) => {
const response = await apiContext.get('/users/protected', {
headers: { 'Authorization': 'Bearer invalid-token' },
});
expect(response.status()).toBe(401);
});
});
```
### 9. Visual Regression Testing
**tests/e2e/visual/homepage.spec.ts**:
```typescript
import { test, expect } from '@playwright/test';
test.describe('Visual Regression', () => {
test('homepage should match snapshot', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixels: 100,
});
});
test('login page should match snapshot', async ({ page }) => {
await page.goto('/login');
await expect(page).toHaveScreenshot('login.png', {
mask: [page.locator('.timestamp')], // Mask dynamic content
});
});
test('mobile viewport should match snapshot', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');
await expect(page).toHaveScreenshot('homepage-mobile.png');
});
});
```
### 10. Performance Testing
**tests/e2e/performance/metrics.spec.ts**:
```typescript
import { test, expect } from '@playwright/test';
test.describe('Performance Metrics', () => {
test('should meet Core Web Vitals thresholds', async ({ page }) => {
await page.goto('/');
const metrics = await page.evaluate(() => {
return new Promise((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
const values = entries.reduce((acc, entry) => {
acc[entry.name] = entry.value;
return acc;
}, {} as Record<string, number>);
resolve(values);
}).observe({ entryTypes: ['measure', 'navigation'] });
});
});
// Verify performance metrics
expect(metrics['first-contentful-paint']).toBeLessThan(1800); // FCP < 1.8s
expect(metrics['largest-contentful-paint']).toBeLessThan(2500); // LCP < 2.5s
});
test('should load page within 3 seconds', async ({ page }) => {
const startTime = Date.now();
await page.goto('/');
await page.waitForLoadState('load');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(3000);
});
test('should have minimal bundle size', async ({ page }) => {
const response = await page.goto('/');
const transferSize = await response?.request().sizes().then(s => s.responseBodySize);
expect(transferSize).toBeLessThan(500000); // < 500KB
});
});
```
### 11. CI/CD Integration
**GitHub Actions (.github/workflows/e2e.yml)**:
```yaml
name: E2E Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps ${{ matrix.browser }}
- name: Run Playwright tests
run: npx playwright test --project=${{ matrix.browser }}
env:
BASE_URL: http://localhost:3000
USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report-${{ matrix.browser }}
path: playwright-report/
retention-days: 30
- name: Upload test videos
if: failure()
uses: actions/upload-artifact@v3
with:
name: test-videos-${{ matrix.browser }}
path: test-results/**/video.webm
retention-days: 7
test-sharded:
timeout-minutes: 60
runs-on: ubuntu-latest
strategy:
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run sharded tests
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload blob report
uses: actions/upload-artifact@v3
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 1
merge-reports:
if: always()
needs: [test-sharded]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Download all reports
uses: actions/download-artifact@v3
with:
path: all-blob-reports
- name: Merge reports
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: Upload merged report
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
```
### 12. Package Dependencies
```json
{
"devDependencies": {
"@playwright/test": "^1.40.0",
"dotenv": "^16.3.1",
"playwright": "^1.40.0"
},
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"test:e2e:headed": "playwright test --headed",
"test:e2e:chromium": "playwright test --project=chromium",
"test:e2e:firefox": "playwright test --project=firefox",
"test:e2e:webkit": "playwright test --project=webkit",
"test:e2e:mobile": "playwright test --project=mobile-chrome --project=mobile-safari",
"test:e2e:report": "playwright show-report",
"test:e2e:codegen": "playwright codegen"
}
}
```
### 13. Environment Configuration
**.env.example**:
```bash
# Base URLs
BASE_URL=http://localhost:3000
API_BASE_URL=http://localhost:3000/api
# Test credentials
USER_EMAIL=test@example.com
USER_PASSWORD=password123
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=admin123
# CI/CD
CI=false
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=false
```
### 14. Best Practices
**Test Organization**:
- Group tests by feature/domain
- Use descriptive test names
- One assertion per test (when possible)
- Use page objects for reusability
- Keep tests independent
**Reliability**:
- Use auto-waiting features
- Avoid hard-coded waits (`page.waitForTimeout`)
- Use data-testid attributes
- Retry flaky tests automatically
- Clean up test data
**Performance**:
- Run tests in parallel
- Use test sharding for large suites
- Reuse authentication state
- Block unnecessary resources
- Use fast selectors (data-testid > text)
**Debugging**:
- Use Playwright Inspector (`--debug`)
- Record traces on failure
- Take screenshots on failure
- Use UI mode for interactive debugging
- Check network logs
## Workflow
1. Ask about application type and E2E requirements
2. Install Playwright and dependencies
3. Create playwright.config.ts with all browsers
4. Set up page object model structure
5. Create custom fixtures for auth and API
6. Implement global setup/teardown
7. Create authentication setup tests
8. Generate test utilities and helpers
9. Write example E2E test suite
10. Configure CI/CD pipeline
11. Set up visual regression testing
12. Add performance testing
13. Provide debugging and maintenance guide
## When to Use
- Setting up E2E testing from scratch
- Migrating from Selenium/Cypress to Playwright
- Adding cross-browser testing
- Implementing visual regression testing
- Setting up CI/CD for E2E tests
- Adding performance testing
- Creating page object model architecture
- Implementing authentication testing
Set up production-ready Playwright E2E testing with comprehensive coverage!