| name | web-testing-automation |
| description | Comprehensive web application testing automation using Playwright, including MCP integration, browser automation, E2E testing, visual regression testing, API testing, and CI/CD integration. Use when creating automated tests, setting up test frameworks, debugging test failures, implementing Page Object Model, or integrating testing into deployment pipelines. |
Web Testing Automation with Playwright
Overview
This skill provides comprehensive guidance for automated web application testing using Playwright and related tools, including Microsoft's Playwright MCP integration for AI-powered testing capabilities.
Core Technologies
1. Playwright - Primary Testing Framework
Why Playwright?
- Cross-browser support (Chromium, Firefox, WebKit)
- Auto-wait capabilities (no more flaky tests)
- Network interception and mocking
- Mobile emulation
- Screenshot and video recording
- Built-in test runner with parallel execution
Installation:
# Initialize new project with Playwright
npm init playwright@latest
# Or add to existing project
npm install -D @playwright/test
npx playwright install
# Install browsers
npx playwright install chromium firefox webkit
2. Playwright MCP (Model Context Protocol)
Microsoft's Playwright MCP allows AI-powered test generation and execution through Claude.
Installation:
# Install the Playwright MCP server
npx @playwright/mcp-server@latest
Configuration for Claude: Add to your Claude MCP settings:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp-server"]
}
}
}
Project Structure
tests/
├── playwright.config.ts # Configuration
├── tests/
│ ├── auth/
│ │ ├── login.spec.ts # Login tests
│ │ └── registration.spec.ts # Registration tests
│ ├── e2e/
│ │ ├── checkout.spec.ts # E2E workflows
│ │ └── user-journey.spec.ts
│ ├── api/
│ │ └── api-tests.spec.ts # API tests
│ └── visual/
│ └── visual-regression.spec.ts
├── pages/ # Page Object Models
│ ├── BasePage.ts
│ ├── LoginPage.ts
│ └── DashboardPage.ts
├── fixtures/ # Test data
│ ├── users.json
│ └── products.json
└── utils/ # Helper utilities
├── test-helpers.ts
└── custom-matchers.ts
Configuration (playwright.config.ts)
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
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/junit.xml' }]
],
use: {
// Configure based on your application type:
// ColdFusion: http://localhost:8500
// React: http://localhost:51xx (e.g., 5100)
// PHP: http://localhost:4000
baseURL: process.env.BASE_URL || 'http://localhost:8500',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10000,
navigationTimeout: 30000,
},
projects: [
// Desktop browsers
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Mobile emulation
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
// Optional: Auto-start your local dev server
// Uncomment and configure for your application type
// webServer: {
// command: 'npm run start', // or 'php -S localhost:4000'
// url: 'http://localhost:4000',
// reuseExistingServer: !process.env.CI,
// },
});
Page Object Model (POM) Pattern
BasePage.ts
import { Page, Locator } from '@playwright/test';
export class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async navigate(path: string) {
await this.page.goto(path);
}
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(name: string) {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
async fillField(locator: string, value: string) {
await this.page.fill(locator, value);
}
async clickButton(locator: string) {
await this.page.click(locator);
}
async getText(locator: string): Promise<string> {
return await this.page.textContent(locator) || '';
}
async waitForElement(locator: string, timeout: number = 5000) {
await this.page.waitForSelector(locator, { timeout });
}
async isVisible(locator: string): Promise<boolean> {
return await this.page.isVisible(locator);
}
}
LoginPage.ts
import { Page, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
// Locators
readonly usernameInput: string = '#username';
readonly passwordInput: string = '#password';
readonly loginButton: string = 'button[type="submit"]';
readonly errorMessage: string = '.error-message';
readonly successMessage: string = '.success-message';
constructor(page: Page) {
super(page);
}
async goto() {
await this.navigate('/login');
await this.waitForPageLoad();
}
async login(username: string, password: string) {
await this.fillField(this.usernameInput, username);
await this.fillField(this.passwordInput, password);
await this.clickButton(this.loginButton);
}
async getErrorMessage(): Promise<string> {
await this.waitForElement(this.errorMessage);
return await this.getText(this.errorMessage);
}
async isLoginSuccessful(): Promise<boolean> {
return await this.isVisible(this.successMessage);
}
async verifyLoginPage() {
await expect(this.page).toHaveURL(/.*login/);
await expect(this.page.locator(this.usernameInput)).toBeVisible();
await expect(this.page.locator(this.passwordInput)).toBeVisible();
}
}
Test Examples
Basic E2E Test
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('User Authentication', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('should login with valid credentials', async ({ page }) => {
await loginPage.login('testuser@example.com', 'Password123!');
// Verify successful login
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('.welcome-message')).toContainText('Welcome');
});
test('should show error with invalid credentials', async () => {
await loginPage.login('invalid@example.com', 'wrongpassword');
// Verify error message
const errorMessage = await loginPage.getErrorMessage();
expect(errorMessage).toContain('Invalid credentials');
});
test('should validate empty fields', async ({ page }) => {
await loginPage.clickButton(loginPage.loginButton);
// Check validation messages
await expect(page.locator('input:invalid')).toHaveCount(2);
});
});
API Testing
import { test, expect } from '@playwright/test';
test.describe('API Tests', () => {
const baseURL = 'https://api.example.com';
test('should fetch user data', async ({ request }) => {
const response = await request.get(`${baseURL}/users/1`);
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const data = await response.json();
expect(data).toHaveProperty('id', 1);
expect(data).toHaveProperty('email');
});
test('should create new user', async ({ request }) => {
const newUser = {
name: 'Test User',
email: 'test@example.com',
password: 'SecurePass123!'
};
const response = await request.post(`${baseURL}/users`, {
data: newUser
});
expect(response.status()).toBe(201);
const data = await response.json();
expect(data.email).toBe(newUser.email);
expect(data).toHaveProperty('id');
});
test('should handle authentication', async ({ request }) => {
// Login to get token
const loginResponse = await request.post(`${baseURL}/auth/login`, {
data: {
email: 'test@example.com',
password: 'password123'
}
});
const { token } = await loginResponse.json();
// Use token for authenticated request
const response = await request.get(`${baseURL}/users/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
expect(response.ok()).toBeTruthy();
});
});
Visual Regression Testing
import { test, expect } from '@playwright/test';
test.describe('Visual Regression Tests', () => {
test('homepage should match snapshot', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// Take screenshot and compare
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixels: 100
});
});
test('button should match snapshot', async ({ page }) => {
await page.goto('/components');
const button = page.locator('.primary-button').first();
await expect(button).toHaveScreenshot('primary-button.png');
});
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', {
fullPage: true
});
});
});
Network Interception and Mocking
import { test, expect } from '@playwright/test';
test.describe('Network Mocking', () => {
test('should mock API response', async ({ page }) => {
// Intercept API call and return mock data
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Mock User 1' },
{ id: 2, name: 'Mock User 2' }
])
});
});
await page.goto('/users');
// Verify mock data is displayed
await expect(page.locator('.user-list')).toContainText('Mock User 1');
});
test('should handle API errors', async ({ page }) => {
// Mock API error
await page.route('**/api/users', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Internal Server Error' })
});
});
await page.goto('/users');
// Verify error handling
await expect(page.locator('.error-message')).toBeVisible();
});
test('should intercept and log requests', async ({ page }) => {
const requests: string[] = [];
// Log all API requests
page.on('request', request => {
if (request.url().includes('/api/')) {
requests.push(request.url());
}
});
await page.goto('/dashboard');
// Verify expected API calls were made
expect(requests).toContain(expect.stringContaining('/api/user'));
expect(requests).toContain(expect.stringContaining('/api/stats'));
});
});
Advanced Testing Patterns
Custom Fixtures
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
type MyFixtures = {
loginPage: LoginPage;
authenticatedPage: Page;
};
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
authenticatedPage: async ({ page }, use) => {
// Automatically login before each test
await page.goto('/login');
await page.fill('#username', 'testuser@example.com');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
await use(page);
},
});
export { expect } from '@playwright/test';
Test Data Management
fixtures/users.json:
{
"validUser": {
"email": "test@example.com",
"password": "SecurePassword123!",
"name": "Test User"
},
"adminUser": {
"email": "admin@example.com",
"password": "AdminPass123!",
"name": "Admin User",
"role": "admin"
}
}
Using Test Data:
import { test, expect } from '@playwright/test';
import users from '../fixtures/users.json';
test('login with test data', async ({ page }) => {
await page.goto('/login');
await page.fill('#email', users.validUser.email);
await page.fill('#password', users.validUser.password);
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/.*dashboard/);
});
Debugging Tests
Debug Mode
# Run in debug mode with Playwright Inspector
npx playwright test --debug
# Run specific test in debug mode
npx playwright test login.spec.ts --debug
# Debug from specific line
npx playwright test --debug-line 25
Trace Viewer
// playwright.config.ts
use: {
trace: 'on-first-retry', // or 'on', 'off', 'retain-on-failure'
}
// View trace
// npx playwright show-trace trace.zip
Screenshots and Videos
test('capture on failure', async ({ page }) => {
// Automatic screenshot on failure if configured
await page.goto('/');
// Manual screenshot
await page.screenshot({ path: 'screenshot.png', fullPage: true });
});
// playwright.config.ts
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
}
CI/CD Integration
GitHub Actions
name: Playwright Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
env:
BASE_URL: ${{ secrets.BASE_URL }}
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Upload test videos
uses: actions/upload-artifact@v3
if: failure()
with:
name: test-videos
path: test-results/
Docker Integration
FROM mcr.microsoft.com/playwright:v1.40.0-focal
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
Performance Testing
import { test, expect } from '@playwright/test';
test('page load performance', async ({ page }) => {
const startTime = Date.now();
await page.goto('/');
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
console.log(`Page loaded in ${loadTime}ms`);
// Assert load time is acceptable
expect(loadTime).toBeLessThan(3000);
});
test('API response time', async ({ request }) => {
const startTime = Date.now();
const response = await request.get('/api/users');
const responseTime = Date.now() - startTime;
console.log(`API responded in ${responseTime}ms`);
expect(response.ok()).toBeTruthy();
expect(responseTime).toBeLessThan(500);
});
Accessibility Testing
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility Tests', () => {
test('should not have accessibility violations', async ({ page }) => {
await page.goto('/');
const accessibilityScanResults = await new AxeBuilder({ page })
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('should have proper ARIA labels', async ({ page }) => {
await page.goto('/');
const button = page.locator('button.primary');
await expect(button).toHaveAttribute('aria-label');
});
test('should be keyboard navigable', async ({ page }) => {
await page.goto('/');
// Test tab navigation
await page.keyboard.press('Tab');
const firstFocusable = await page.evaluate(() =>
document.activeElement?.tagName
);
expect(firstFocusable).toBeTruthy();
});
});
Best Practices
1. Use Stable Selectors
// ❌ Bad - Fragile selectors
await page.click('div > div > button:nth-child(2)');
await page.click('.css-1234567');
// ✅ Good - Stable selectors
await page.click('[data-testid="submit-button"]');
await page.click('button[aria-label="Submit form"]');
await page.click('button:has-text("Submit")');
2. Auto-Waiting
// ❌ Bad - Manual waits
await page.waitForTimeout(5000);
await page.click('button');
// ✅ Good - Let Playwright auto-wait
await page.click('button'); // Automatically waits for button to be actionable
3. Assertions
// ✅ Use specific assertions
await expect(page.locator('.message')).toBeVisible();
await expect(page.locator('.message')).toContainText('Success');
await expect(page).toHaveURL(/.*dashboard/);
await expect(page).toHaveTitle('Dashboard');
4. Test Isolation
// Each test should be independent
test.beforeEach(async ({ page }) => {
// Reset state
await page.goto('/');
// Clear cookies/storage if needed
await page.context().clearCookies();
});
Using Playwright MCP with Claude
When Playwright MCP is installed, you can ask Claude to:
Generate tests from descriptions:
- "Create a test that verifies login with valid credentials"
- "Write tests for the checkout flow"
Debug failing tests:
- "This test is failing: [paste test code]. Help me debug and fix it."
Convert manual test cases:
- "Convert these manual test steps into Playwright tests"
Improve existing tests:
- "Review this test and suggest improvements"
Common Issues and Solutions
Flaky Tests
// Issue: Test sometimes passes, sometimes fails
// ❌ Bad
await page.click('button');
const text = await page.textContent('.result'); // May not be ready
// ✅ Good - Wait for element
await page.click('button');
await expect(page.locator('.result')).toContainText('Expected');
Timeout Issues
// Increase timeout for specific actions
await page.click('button', { timeout: 30000 });
// Or in config
test.setTimeout(60000);
Element Not Found
// Wait for element to be available
await page.waitForSelector('.my-element', { state: 'visible' });
// Or use getBy methods with auto-wait
await page.getByRole('button', { name: 'Submit' }).click();
Testing Checklist
- Tests are isolated and independent
- Using stable, semantic selectors
- Page Object Model implemented
- API tests cover main endpoints
- Visual regression tests for critical UI
- Accessibility tests included
- Mobile viewport tests
- Cross-browser tests configured
- CI/CD pipeline integrated
- Test data fixtures organized
- Error scenarios covered
- Performance metrics validated
- Screenshots/videos on failure
- Tests run in parallel
Useful Commands
# Run all tests
npx playwright test
# Run specific test file
npx playwright test login.spec.ts
# Run tests in headed mode
npx playwright test --headed
# Run tests in specific browser
npx playwright test --project=chromium
# Run in debug mode
npx playwright test --debug
# Generate code (record actions)
npx playwright codegen https://example.com
# Show test report
npx playwright show-report
# View trace
npx playwright show-trace trace.zip
# Update snapshots
npx playwright test --update-snapshots
Notes for Claude
When helping users with Playwright testing:
- Prefer Page Object Model for maintainability
- Use auto-waiting instead of manual waits
- Write atomic tests that are independent
- Use semantic selectors (data-testid, role, text)
- Include proper error handling and debugging info
- Suggest visual regression tests for UI components
- Recommend CI/CD integration for automated testing
- Check for accessibility in test suggestions
- Consider mobile viewports when relevant
- Use MCP features when available for enhanced capabilities