28 KiB
/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):
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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):
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
{
"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:
# 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
- Ask about application type and E2E requirements
- Install Playwright and dependencies
- Create playwright.config.ts with all browsers
- Set up page object model structure
- Create custom fixtures for auth and API
- Implement global setup/teardown
- Create authentication setup tests
- Generate test utilities and helpers
- Write example E2E test suite
- Configure CI/CD pipeline
- Set up visual regression testing
- Add performance testing
- 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!