Claude Code Plugins

Community-maintained marketplace

Feedback

web-testing-automation

@ErnestPenaJr/Project-32
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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:

  1. Generate tests from descriptions:

    • "Create a test that verifies login with valid credentials"
    • "Write tests for the checkout flow"
  2. Debug failing tests:

    • "This test is failing: [paste test code]. Help me debug and fix it."
  3. Convert manual test cases:

    • "Convert these manual test steps into Playwright tests"
  4. 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:

  1. Prefer Page Object Model for maintainability
  2. Use auto-waiting instead of manual waits
  3. Write atomic tests that are independent
  4. Use semantic selectors (data-testid, role, text)
  5. Include proper error handling and debugging info
  6. Suggest visual regression tests for UI components
  7. Recommend CI/CD integration for automated testing
  8. Check for accessibility in test suggestions
  9. Consider mobile viewports when relevant
  10. Use MCP features when available for enhanced capabilities