Playwright Testing
> **Skill Purpose:** E2E test setup, component testing patterns, and responsive testing automation
SKILL.md
Full skill instructions
Playwright Testing
Skill Purpose: E2E test setup, component testing patterns, and responsive testing automation
Core Skill Pattern
Objective: Establish comprehensive E2E testing strategy with Playwright, component testing, and responsive validation.
Universal Pattern:
- Define testing strategy and scope
- Create Playwright configuration and setup
- Establish E2E testing patterns and procedures
- Set up component testing framework
- Create responsive and accessibility testing automation
Key Decisions (Project-Specific):
- Testing scope and coverage requirements
- Browser and device targets for testing
- Test environment and data management
- CI/CD integration and parallel execution
- Reporting and test result management
Project-Specific Implementation Notes
Customize per project:
- Browser targets based on user analytics and requirements
- Test scope based on application complexity and critical paths
- Test data strategy based on data privacy and complexity
- Parallel execution based on CI/CD resources
- Reporting approach based on team needs and compliance
Example Implementation (Next.js Playwright Pattern)
Note: This is an example pattern using Playwright with Next.js. Adapt testing framework and patterns based on your specific project requirements and testing needs.
Prerequisites (Example)
- Next.js project structure established
- Playwright installed and configured
- Test environment and database setup
- CI/CD pipeline configured for automated testing
Example: Playwright Testing Implementation
Framework-Specific Example: This demonstrates Playwright testing patterns with Next.js. Adapt for your testing framework and application requirements.
Playwright 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',
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/results.xml' }],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
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 12'] },
},
{
name: 'Tablet',
use: { ...devices['iPad Pro'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Test Utilities and Helpers
// e2e/utils/test-helpers.ts
import { expect, Page, TestInfo } from '@playwright/test';
export class TestHelpers {
static async login(page: Page, email: string, password: string) {
await page.goto('/login');
await page.fill('[data-testid=email-input]', email);
await page.fill('[data-testid=password-input]', password);
await page.click('[data-testid=login-button]');
// Wait for navigation to dashboard
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid=user-menu]')).toBeVisible();
}
static async logout(page: Page) {
await page.click('[data-testid=user-menu]');
await page.click('[data-testid=logout-button]');
await expect(page).toHaveURL('/login');
}
static async waitForAPIResponse(page: Page, url: string) {
return page.waitForResponse(response =>
response.url().includes(url)
);
}
static async takeScreenshot(page: Page, name: string, testInfo: TestInfo) {
const screenshot = await page.screenshot({
fullPage: true,
path: `test-results/${name}-${testInfo.retry}.png`
});
await testInfo.attachments.push({
name: name,
contentType: 'image/png',
body: screenshot
});
}
static async checkAccessibility(page: Page) {
// This would integrate with axe-core for accessibility testing
await page.injectTag({
path: 'node_modules/axe-core/axe.min.js'
});
const results = await page.evaluate(() => {
// @ts-ignore
return axe.run();
});
expect(results.violations).toHaveLength(0);
}
static async generateTestData() {
return {
user: {
email: `test-${Date.now()}@example.com`,
name: 'Test User',
password: 'TestPassword123!',
},
product: {
name: 'Test Product',
price: 29.99,
description: 'A test product for E2E testing',
}
};
}
}
E2E Test Examples
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
import { TestHelpers } from './utils/test-helpers';
test.describe('Authentication', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should allow user to login with valid credentials', async ({ page }) => {
const testData = await TestHelpers.generateTestData();
// Navigate to login
await page.click('[data-testid=login-link]');
await expect(page).toHaveURL('/login');
// Fill login form
await page.fill('[data-testid=email-input]', testData.user.email);
await page.fill('[data-testid=password-input]', testData.user.password);
await page.click('[data-testid=login-button]');
// Verify successful login
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid=welcome-message]')).toContainText('Welcome');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.click('[data-testid=login-link]');
await page.fill('[data-testid=email-input]', '[email protected]');
await page.fill('[data-testid=password-input]', 'wrongpassword');
await page.click('[data-testid=login-button]');
// Should stay on login page and show error
await expect(page).toHaveURL('/login');
await expect(page.locator('[data-testid=error-message]')).toBeVisible();
await expect(page.locator('[data-testid=error-message]')).toContainText('Invalid credentials');
});
test('should allow user to logout', async ({ page }) => {
const testData = await TestHelpers.generateTestData();
// First login
await TestHelpers.login(page, testData.user.email, testData.user.password);
// Then logout
await TestHelpers.logout(page);
// Verify logged out state
await expect(page.locator('[data-testid=login-link]')).toBeVisible();
});
});
Responsive Testing
// e2e/responsive.spec.ts
import { test, expect, devices } from '@playwright/test';
const breakpoints = [
{ name: 'mobile', device: devices['iPhone 12'] },
{ name: 'tablet', device: devices['iPad Pro'] },
{ name: 'desktop', device: devices['Desktop Chrome'] },
];
breakpoints.forEach(({ name, device }) => {
test.describe(`Responsive - ${name}`, () => {
test.use({ ...device });
test('should display navigation correctly', async ({ page }) => {
await page.goto('/');
if (name === 'mobile') {
// Mobile should show hamburger menu
await expect(page.locator('[data-testid=mobile-menu-button]')).toBeVisible();
await expect(page.locator('[data-testid=desktop-nav]')).toBeHidden();
// Test mobile menu interaction
await page.click('[data-testid=mobile-menu-button]');
await expect(page.locator('[data-testid=mobile-menu]')).toBeVisible();
} else {
// Tablet and desktop should show full navigation
await expect(page.locator('[data-testid=desktop-nav]')).toBeVisible();
await expect(page.locator('[data-testid=mobile-menu-button]')).toBeHidden();
}
});
test('should display content correctly', async ({ page }) => {
await page.goto('/products');
// Check that product grid adapts to screen size
const productGrid = page.locator('[data-testid=product-grid]');
await expect(productGrid).toBeVisible();
if (name === 'mobile') {
// Mobile should have 1 column
await expect(productGrid.locator('[data-testid=product-card]').first()).toBeVisible();
} else if (name === 'tablet') {
// Tablet should have 2 columns
const gridColumns = await productGrid.evaluate(el =>
getComputedStyle(el).gridTemplateColumns
);
expect(gridColumns).toContain('repeat(2');
} else {
// Desktop should have 3-4 columns
const gridColumns = await productGrid.evaluate(el =>
getComputedStyle(el).gridTemplateColumns
);
expect(gridColumns).toContain('repeat(3');
}
});
test('should handle touch interactions on mobile', async ({ page }) => {
if (name !== 'mobile') return;
await page.goto('/');
// Test swipe gestures if applicable
const carousel = page.locator('[data-testid=carousel]');
if (await carousel.isVisible()) {
await carousel.swipe('left');
await page.waitForTimeout(500);
// Verify carousel moved to next slide
const activeSlide = carousel.locator('[data-testid=carousel-slide].active');
await expect(activeSlide).toBeVisible();
}
});
});
});
Component Testing with Playwright
// e2e/components/button.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Button Component', () => {
test('should be accessible', async ({ page }) => {
await page.goto('/components/button');
const button = page.locator('[data-testid=test-button]');
await expect(button).toBeVisible();
// Check accessibility attributes
await expect(button).toHaveAttribute('role', 'button');
await expect(button).toHaveAttribute('tabindex', '0');
// Test keyboard navigation
await button.focus();
await expect(button).toBeFocused();
await page.keyboard.press('Enter');
await expect(page.locator('[data-testid=button-click-result]')).toContainText('Button clicked');
});
test('should handle different states', async ({ page }) => {
await page.goto('/components/button');
// Test disabled state
const disabledButton = page.locator('[data-testid=disabled-button]');
await expect(disabledButton).toBeDisabled();
await expect(disabledButton).toHaveCSS('opacity', '0.5');
// Test loading state
const loadingButton = page.locator('[data-testid=loading-button]');
await loadingButton.click();
await expect(loadingButton.locator('[data-testid=spinner]')).toBeVisible();
await expect(loadingButton).toBeDisabled();
});
test('should have proper color contrast', async ({ page }) => {
await page.goto('/components/button');
const button = page.locator('[data-testid=test-button]');
// Get computed styles for color contrast testing
const styles = await button.evaluate(el => {
const computed = getComputedStyle(el);
return {
color: computed.color,
backgroundColor: computed.backgroundColor,
};
});
// This would integrate with a color contrast checker
// For now, just verify the styles exist
expect(styles.color).toBeTruthy();
expect(styles.backgroundColor).toBeTruthy();
});
});
API Integration Testing
// e2e/api-integration.spec.ts
import { test, expect } from '@playwright/test';
import { TestHelpers } from './utils/test-helpers';
test.describe('API Integration', () => {
test('should load data from API', async ({ page }) => {
await page.goto('/products');
// Wait for API response
const response = await TestHelpers.waitForAPIResponse(page, '/api/products');
expect(response.status()).toBe(200);
// Verify data is displayed
await expect(page.locator('[data-testid=product-grid]')).toBeVisible();
const productCards = page.locator('[data-testid=product-card]');
await expect(productCards.first()).toBeVisible();
});
test('should handle API errors gracefully', async ({ page }) => {
// Mock API error
await page.route('/api/products', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal server error' })
});
});
await page.goto('/products');
// Should show error state
await expect(page.locator('[data-testid=error-message]')).toBeVisible();
await expect(page.locator('[data-testid=error-message]')).toContainText('Failed to load products');
});
test('should handle form submissions', async ({ page }) => {
const testData = await TestHelpers.generateTestData();
await page.goto('/register');
// Fill registration form
await page.fill('[data-testid=name-input]', testData.user.name);
await page.fill('[data-testid=email-input]', testData.user.email);
await page.fill('[data-testid=password-input]', testData.user.password);
// Mock API response
await page.route('/api/auth/register', route => {
route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({
success: true,
user: { id: 1, email: testData.user.email }
})
});
});
await page.click('[data-testid=register-button]');
// Verify successful registration
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid=success-message]')).toBeVisible();
});
});
Performance Testing
// e2e/performance.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Performance', () => {
test('should load within performance budget', async ({ page }) => {
const startTime = Date.now();
await page.goto('/');
// Wait for page to be fully loaded
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
// Performance budget: 3 seconds
expect(loadTime).toBeLessThan(3000);
});
test('should have good Core Web Vitals', async ({ page }) => {
const metrics = await page.evaluate(() => {
return new Promise((resolve) => {
// @ts-ignore
new PerformanceObserver((list) => {
const entries = list.getEntries();
resolve({
lcp: entries.find(entry => entry.name === 'largest-contentful-paint')?.startTime || 0,
fid: entries.find(entry => entry.name === 'first-input')?.processingStart - entries.find(entry => entry.name === 'first-input')?.startTime || 0,
cls: entries.find(entry => entry.name === 'cumulative-layout-shift')?.value || 0,
});
}).observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'cumulative-layout-shift'] });
});
});
// Core Web Vitals thresholds
expect(metrics.lcp).toBeLessThan(2500); // LCP: 2.5s
expect(metrics.fid).toBeLessThan(100); // FID: 100ms
expect(metrics.cls).toBeLessThan(0.1); // CLS: 0.1
});
});
Test Data Management
// e2e/fixtures/test-data.ts
import { test as base } from '@playwright/test';
type TestFixtures = {
testData: {
user: { email: string; name: string; password: string };
product: { name: string; price: number; description: string };
};
};
export const test = base.extend<TestFixtures>({
testData: async ({}, use) => {
const data = {
user: {
email: `test-${Date.now()}@example.com`,
name: 'Test User',
password: 'TestPassword123!',
},
product: {
name: 'Test Product',
price: 29.99,
description: 'A test product for E2E testing',
}
};
await use(data);
},
});
export { expect };
Best Practices
- Page Object Model - Separate page interactions from test logic
- Test data isolation - Use unique test data to avoid conflicts
- Parallel execution - Structure tests to run in parallel
- Retry logic - Use retries for flaky network-dependent tests
- Accessibility testing - Include accessibility checks in E2E tests
Stop Conditions
STOP and escalate if:
- Testing strategy unclear or incomplete
- Test environment not properly configured
- Browser/device coverage not defined
- CI/CD integration not established
Skill Version: 1.0.0
