All Skills

Use when testing frontend applications. AI-assisted browser testing with Playwright MCP. Fast, deterministic, no vision models needed.

l
$npx skills add liauw-media/CodeAssist --skill playwright-frontend-testing

Playwright Frontend Testing

Core Principle

Test real browser behavior with AI assistance through Playwright's accessibility tree.

Overview

Playwright MCP (Model Context Protocol) enables AI-assisted frontend testing by exposing browser interactions through structured data instead of screenshots. This makes tests fast, deterministic, and LLM-friendly without requiring vision models.

When to Use This Skill

  • Testing web application user interfaces
  • End-to-end testing with real browsers
  • Cross-browser compatibility testing
  • Testing interactive features (forms, buttons, navigation)
  • Accessibility testing
  • Brand compliance testing (verify colors, fonts match guidelines)
  • Visual regression testing
  • Integration testing with real browser state

Why Playwright MCP?

Traditional Approach (Screenshots):

  • ❌ Slow (large image processing)
  • ❌ Non-deterministic (OCR, vision models)
  • ❌ Expensive (vision model costs)
  • ❌ Brittle (pixel-based matching)

Playwright MCP Approach (Accessibility Tree):

  • ✅ Fast (structured data)
  • ✅ Deterministic (semantic interactions)
  • ✅ Lightweight (no vision models)
  • ✅ LLM-friendly (text-based)
  • ✅ Accessibility-first (follows a11y best practices)
  • Headless-ready (works on VPS/servers without display)

Works anywhere:

  • 💻 Local development (headed mode for debugging)
  • 🖥️ VPS/Headless Servers (Claude Code CLI, no display required)
  • 🔄 CI/CD pipelines (automated testing)
  • 🌐 Remote SSH sessions (full browser testing over SSH)

⭐ ALWAYS use this workflow for the best results:

Phase 1: Exploratory Testing with MCP (Discovery)

Purpose: Find issues quickly through interactive testing

1. Use Playwright MCP interactively with AI assistance
2. Navigate application, test features manually
3. Document bugs/issues as you find them
4. Iterate quickly - no test files needed yet

Benefits:

  • ✅ Fast discovery - find issues immediately
  • ✅ Interactive - adjust approach based on findings
  • ✅ No maintenance overhead during exploration
  • ✅ Good for understanding application behavior
  • ✅ AI-assisted - Claude helps navigate via accessibility tree

When to use:

  • First-time testing of new features
  • Exploring unfamiliar codebases
  • Quick smoke testing
  • Finding UI/UX issues

Phase 2: Write Permanent Test Suite (CI/CD)

Purpose: Lock in findings as regression protection

1. Based on MCP exploration, write .spec.js/.spec.ts files
2. Cover critical paths discovered during exploration
3. Add to CI/CD pipeline
4. Team can run tests locally

Benefits:

  • ✅ Permanent regression protection
  • ✅ Runs in CI/CD automatically
  • ✅ Prevents future bugs
  • ✅ Team collaboration
  • ✅ Documentation of expected behavior

When to use:

  • After exploratory testing finds issues
  • For critical user flows
  • When code is changing frequently
  • For long-term quality assurance

The Hybrid Workflow

Step 1: MCP Exploration (Quick Discovery)
→ Use Playwright MCP interactively
→ Find bugs, understand flows
→ Document issues found

Step 2: Write Permanent Tests (Lock It In)
→ Create .spec.ts files for critical paths
→ Add assertions for bugs found in Step 1
→ Commit to repository

Step 3: CI/CD Integration (Prevent Regressions)
→ Tests run on every commit
→ Catch regressions automatically
→ Team protected from breaking changes

Example workflow:

# Phase 1: Interactive exploration
# Use Claude Code with Playwright MCP
# Test login flow, find validation bug

# Phase 2: Write permanent test
cat > tests/auth.spec.ts << 'EOF'
test('login validates email format', async ({ page }) => {
  await page.goto('https://app.example.com/login');
  await page.fill('[name="email"]', 'invalid-email');
  await page.click('button[type="submit"]');

  // Bug found during MCP exploration: error message missing
  await expect(page.locator('.error')).toContainText('Invalid email format');
});
EOF

# Phase 3: Run in CI/CD
# Add to .github/workflows/test.yml
# Tests now run on every PR

Why Hybrid is Best

ApproachSpeedPermanenceCI/CDBest For
MCP Only⚡ Fast❌ No❌ NoQuick audits, exploration
Tests Only🐢 Slow✅ Yes✅ YesKnown requirements
Hybrid⚡🔒 BothYesYesEverything

The Iron Law: Never write permanent tests blindly - explore with MCP first to understand what actually needs testing.

Installation & Setup

Step 1: Install Playwright MCP

For MCP-compatible tools (Claude Desktop, VS Code, Cursor):

Add to MCP configuration file:

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}

Configuration file locations:

  • Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json (Mac)
  • VS Code: .vscode/settings.json (project) or user settings
  • Cursor: Similar to VS Code
  • Claude Code (CLI): .claude/mcp_config.json (project) or ~/.config/claude-code/mcp_config.json (global)

Step 2: Configure for Claude Code CLI (Headless Servers/VPS)

Claude Code works perfectly on headless servers - the MCP server runs browsers in headless mode without requiring a display.

Recommended configuration for VPS/headless servers:

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "@playwright/mcp@latest",
        "--headless",
        "--browser", "chromium"
      ]
    }
  }
}

For servers without X11/display:

# Install dependencies (Ubuntu/Debian)
sudo apt-get update
sudo apt-get install -y \
    libnss3 \
    libnspr4 \
    libatk1.0-0 \
    libatk-bridge2.0-0 \
    libcups2 \
    libdrm2 \
    libdbus-1-3 \
    libxkbcommon0 \
    libatspi2.0-0 \
    libxcomposite1 \
    libxdamage1 \
    libxfixes3 \
    libxrandr2 \
    libgbm1 \
    libpango-1.0-0 \
    libcairo2 \
    libasound2

# Install Playwright browsers (headless)
npx playwright install chromium

Verify headless mode works:

# Test headless browser
npx playwright test --headed=false

# Or with MCP via Claude Code
# Playwright MCP automatically uses headless mode on servers without displays

Key benefits for VPS/server usage:

  • ✅ No display/X11 required
  • ✅ Runs in background
  • ✅ Perfect for CI/CD pipelines
  • ✅ Lower resource usage than headed mode
  • ✅ Claude Code CLI works identically on server and local

Step 3: Configure Browser Options (General)

Headless mode (CI/CD):

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "@playwright/mcp@latest",
        "--headless",
        "--browser", "chromium"
      ]
    }
  }
}

Headed mode (development, debugging):

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "@playwright/mcp@latest",
        "--browser", "chromium",
        "--viewport-size", "1280x720"
      ]
    }
  }
}

Persistent profile (keep login state):

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "@playwright/mcp@latest",
        "--user-data-dir", "./playwright-profile"
      ]
    }
  }
}

Isolated sessions (fresh each time):

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "@playwright/mcp@latest",
        "--isolated"
      ]
    }
  }
}

Step 3: Install Playwright in Project

# For Node.js projects
npm install --save-dev @playwright/test

# For Python projects
pip install playwright
playwright install

# For other languages
# See: https://playwright.dev/docs/intro

Step 4: Initialize Playwright Tests

# Node.js
npx playwright install
npx playwright test --init

# Creates:
# - playwright.config.ts
# - tests/ directory
# - example.spec.ts

Core Playwright MCP Capabilities

1. Browser Navigation

// Navigate to URL
await page.goto('https://example.com');

// Go back/forward
await page.goBack();
await page.goForward();

// Reload page
await page.reload();

2. Element Interactions

AI-assisted clicking (via MCP):

Ask AI: "Click the login button"
→ AI uses accessibility tree to find button
→ Clicks correct element

Programmatic clicking:

// Click by text
await page.click('text=Login');

// Click by role
await page.click('role=button[name="Login"]');

// Click by test ID
await page.click('[data-testid="login-btn"]');

3. Form Filling

// Fill input
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password123');

// Select dropdown
await page.selectOption('select[name="country"]', 'USA');

// Check checkbox
await page.check('input[type="checkbox"][name="terms"]');

// Upload file
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');

4. Assertions

// Element visible
await expect(page.locator('text=Welcome')).toBeVisible();

// Text content
await expect(page.locator('h1')).toHaveText('Dashboard');

// Count elements
await expect(page.locator('.todo-item')).toHaveCount(5);

// URL
await expect(page).toHaveURL('https://example.com/dashboard');

// Screenshot comparison
await expect(page).toHaveScreenshot();

5. Accessibility Tree

Get page snapshot (MCP capability):

Ask AI: "Get accessibility snapshot of the page"
→ Returns structured accessibility tree
→ Shows all interactive elements
→ Includes roles, labels, states

Programmatic access:

const snapshot = await page.accessibility.snapshot();
console.log(JSON.stringify(snapshot, null, 2));

Writing Effective Playwright Tests

Test Structure (AAA Pattern)

import { test, expect } from '@playwright/test';

test('user can login successfully', async ({ page }) => {
  // Arrange: Navigate and setup
  await page.goto('https://example.com/login');

  // Act: Perform actions
  await page.fill('input[name="email"]', 'user@example.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  // Assert: Verify outcomes
  await expect(page).toHaveURL('https://example.com/dashboard');
  await expect(page.locator('text=Welcome back')).toBeVisible();
});

Brand Compliance Testing

Validate that implementation matches brand guidelines.

When Brand Guidelines Exist

Check for .claude/BRAND-GUIDELINES.md before testing:

1. If brand guidelines exist:
   - Read color palette specifications
   - Read typography specifications
   - Read visual style requirements

2. Create tests to verify compliance:
   - Color validation
   - Typography validation
   - Spacing/sizing validation
   - Visual style validation

Brand Validation Tests

Test 1: Color Compliance

test('should use brand colors', async ({ page }) => {
  await page.goto('https://example.com');

  // Read brand guidelines (manually or from file)
  const brandPrimary = '#2563EB';  // From .claude/BRAND-GUIDELINES.md
  const brandSecondary = '#7C3AED';

  // Get computed styles
  const button = page.locator('button[data-testid="primary-cta"]');
  const bgColor = await button.evaluate(el =>
    window.getComputedStyle(el).backgroundColor
  );

  // Convert RGB to HEX and compare
  expect(rgbToHex(bgColor)).toBe(brandPrimary);
});

Test 2: Typography Compliance

test('should use brand typography', async ({ page }) => {
  await page.goto('https://example.com');

  // From brand guidelines
  const brandHeadingFont = 'Playfair Display';
  const brandBodyFont = 'Inter';

  // Check heading font
  const heading = page.locator('h1').first();
  const headingFont = await heading.evaluate(el =>
    window.getComputedStyle(el).fontFamily
  );
  expect(headingFont).toContain(brandHeadingFont);

  // Check body font
  const paragraph = page.locator('p').first();
  const bodyFont = await paragraph.evaluate(el =>
    window.getComputedStyle(el).fontFamily
  );
  expect(bodyFont).toContain(brandBodyFont);
});

Test 3: Spacing System Compliance

test('should use brand spacing system', async ({ page }) => {
  await page.goto('https://example.com');

  // From brand guidelines: --space-md: 1rem (16px)
  const brandSpacingMd = '16px';

  const section = page.locator('section').first();
  const padding = await section.evaluate(el =>
    window.getComputedStyle(el).padding
  );

  // Verify consistent spacing
  expect(padding).toContain(brandSpacingMd);
});

Test 4: Visual Style Compliance

test('should match brand visual style', async ({ page }) => {
  await page.goto('https://example.com');

  // From brand guidelines: Border radius should be --radius-md: 0.5rem (8px)
  const brandBorderRadius = '8px';

  const card = page.locator('[data-testid="card"]').first();
  const borderRadius = await card.evaluate(el =>
    window.getComputedStyle(el).borderRadius
  );

  expect(borderRadius).toBe(brandBorderRadius);
});

Brand Audit Test Suite

Complete test file for brand validation:

// tests/brand-compliance.spec.ts
import { test, expect } from '@playwright/test';

// Load brand guidelines
const BRAND_COLORS = {
  primary: '#2563EB',
  secondary: '#7C3AED',
  accent: '#F59E0B',
};

const BRAND_FONTS = {
  heading: 'Playfair Display',
  body: 'Inter',
};

test.describe('Brand Compliance', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://example.com');
  });

  test('primary buttons use brand primary color', async ({ page }) => {
    const buttons = page.locator('button.btn-primary');
    const count = await buttons.count();

    for (let i = 0; i < count; i++) {
      const bgColor = await buttons.nth(i).evaluate(el =>
        window.getComputedStyle(el).backgroundColor
      );
      expect(rgbToHex(bgColor)).toBe(BRAND_COLORS.primary);
    }
  });

  test('all headings use brand heading font', async ({ page }) => {
    const headings = page.locator('h1, h2, h3, h4, h5, h6');
    const count = await headings.count();

    for (let i = 0; i < count; i++) {
      const fontFamily = await headings.nth(i).evaluate(el =>
        window.getComputedStyle(el).fontFamily
      );
      expect(fontFamily).toContain(BRAND_FONTS.heading);
    }
  });

  test('body text uses brand body font', async ({ page }) => {
    const paragraphs = page.locator('p, span, div');
    const sample = await paragraphs.first().evaluate(el =>
      window.getComputedStyle(el).fontFamily
    );
    expect(sample).toContain(BRAND_FONTS.body);
  });

  test('links use brand accent color', async ({ page }) => {
    const links = page.locator('a');
    const firstLink = links.first();
    const color = await firstLink.evaluate(el =>
      window.getComputedStyle(el).color
    );
    expect(rgbToHex(color)).toBe(BRAND_COLORS.accent);
  });
});

// Helper function
function rgbToHex(rgb: string): string {
  const match = rgb.match(/\d+/g);
  if (!match) return '';
  const [r, g, b] = match.map(Number);
  return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('').toUpperCase();
}

Integration with brand-guidelines Skill

Workflow:

1. brand-guidelines skill creates .claude/BRAND-GUIDELINES.md
2. playwright-frontend-testing reads guidelines
3. Creates validation tests based on guidelines
4. Runs tests to verify compliance
5. Reports deviations as test failures

Announcing brand testing:

I'm using playwright-frontend-testing with brand compliance validation.

Brand guidelines found at .claude/BRAND-GUIDELINES.md:
- Primary color: #2563EB
- Heading font: Playfair Display
- Body font: Inter

I'll create tests to verify these are correctly implemented.

Best Practices

1. Use Semantic Selectors

// ✅ Good: Semantic, accessible
await page.click('role=button[name="Submit"]');
await page.click('text=Login');
await page.click('[data-testid="submit-btn"]');

// ❌ Bad: Brittle, implementation details
await page.click('.btn-primary.submit-button');
await page.click('#form > div > button:nth-child(3)');

2. Wait for Conditions (Not Timeouts)

// ✅ Good: Wait for specific condition
await page.waitForSelector('text=Success', { state: 'visible' });

// ❌ Bad: Arbitrary timeout
await page.waitForTimeout(2000);

Note: This integrates with condition-based-waiting skill!

3. Use Test IDs for Dynamic Content

// HTML
<button data-testid="submit-btn">Submit</button>

// Test
await page.click('[data-testid="submit-btn"]');

4. Isolate Tests

// ✅ Good: Each test is independent
test.beforeEach(async ({ page }) => {
  // Setup fresh state
  await page.goto('https://example.com');
});

// ❌ Bad: Tests depend on each other
// Test 1 creates user
// Test 2 assumes user exists

5. Handle Network Conditions

// Wait for API calls
await page.waitForResponse('**/api/users');

// Mock API responses
await page.route('**/api/users', route => {
  route.fulfill({
    status: 200,
    body: JSON.stringify({ users: [] })
  });
});

// Simulate slow network
await page.route('**/*', route => {
  setTimeout(() => route.continue(), 1000);
});

AI-Assisted Testing with MCP

Workflow

  1. AI analyzes page accessibility tree
  2. AI identifies interactive elements
  3. AI performs actions semantically
  4. AI verifies outcomes

Example Session

You: "Test the login flow on example.com"

AI: "I'm using the playwright-frontend-testing skill to test the login flow.

Step 1: Navigate to login page
[Uses MCP browser_navigate]

Step 2: Get page accessibility snapshot
[Uses MCP to understand page structure]

Found:
- Email input (role: textbox, name: 'Email')
- Password input (role: textbox, name: 'Password')
- Submit button (role: button, name: 'Sign In')

Step 3: Fill credentials
[Uses MCP browser_fill]

Step 4: Click submit
[Uses MCP browser_click]

Step 5: Verify redirect
[Checks URL changed to /dashboard]

Step 6: Verify success message
[Finds 'Welcome' text in accessibility tree]

✅ Login flow test passed"

Configuration File (playwright.config.ts)

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // Test directory
  testDir: './tests',

  // Timeout per test
  timeout: 30000,

  // Retry failed tests
  retries: process.env.CI ? 2 : 0,

  // Run tests in parallel
  workers: process.env.CI ? 1 : undefined,

  // Reporter
  reporter: [
    ['html'],
    ['list'],
    ['junit', { outputFile: 'test-results/junit.xml' }]
  ],

  // Shared settings
  use: {
    // Base URL
    baseURL: 'http://localhost:3000',

    // Screenshot on failure
    screenshot: 'only-on-failure',

    // Video on failure
    video: 'retain-on-failure',

    // Trace on failure
    trace: 'on-first-retry',
  },

  // Browser projects
  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'] },
    },
  ],

  // Web server (optional)
  webServer: {
    command: 'npm run dev',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

Common Testing Patterns

Pattern 1: Login Helper

// tests/helpers/auth.ts
export async function login(page, email, password) {
  await page.goto('/login');
  await page.fill('input[name="email"]', email);
  await page.fill('input[name="password"]', password);
  await page.click('button[type="submit"]');
  await page.waitForURL('**/dashboard');
}

// tests/dashboard.spec.ts
import { test } from '@playwright/test';
import { login } from './helpers/auth';

test('user can access dashboard', async ({ page }) => {
  await login(page, 'user@example.com', 'password123');
  // Test dashboard...
});

Pattern 2: Page Object Model

// pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.page.fill('input[name="email"]', email);
    await this.page.fill('input[name="password"]', password);
    await this.page.click('button[type="submit"]');
  }

  async getErrorMessage() {
    return this.page.locator('.error-message').textContent();
  }
}

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('shows error for invalid credentials', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('invalid@example.com', 'wrong');

  const error = await loginPage.getErrorMessage();
  expect(error).toBe('Invalid email or password');
});

Pattern 3: API Mocking

test('handles API errors gracefully', async ({ page }) => {
  // Mock API to return error
  await page.route('**/api/login', route => {
    route.fulfill({
      status: 500,
      body: JSON.stringify({ error: 'Server error' })
    });
  });

  await page.goto('/login');
  await page.fill('input[name="email"]', 'user@example.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  // Verify error message shown
  await expect(page.locator('.error-message')).toContainText('Server error');
});

Pattern 4: Visual Regression

test('homepage looks correct', async ({ page }) => {
  await page.goto('/');

  // Take screenshot and compare
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    maxDiffPixels: 100
  });
});

Pattern 5: Accessibility Testing

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('page has no accessibility violations', async ({ page }) => {
  await page.goto('/');

  const accessibilityScanResults = await new AxeBuilder({ page }).analyze();

  expect(accessibilityScanResults.violations).toEqual([]);
});

Hybrid Workflow in Practice

Example: Testing E-commerce Checkout Flow

Phase 1: MCP Exploration (Interactive Discovery)

You: "Test the checkout flow on https://shop.example.com"

Claude (via MCP):
1. Navigating to shop.example.com
2. Adding item to cart...
3. Clicking checkout button...
4. Found issue: checkout button disabled without error message
5. Filling shipping form...
6. Found issue: ZIP code doesn't validate Canadian postal codes
7. Submitting payment...
8. Success! Order completed.

Issues found:
- Checkout button provides no feedback when disabled
- ZIP validation rejects valid Canadian postal codes (H3Z 2Y7)
- Success message could be clearer

Phase 2: Write Permanent Tests (Lock In Findings)

// tests/checkout.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Checkout Flow', () => {
  test('shows error when checkout clicked with empty cart', async ({ page }) => {
    // Bug found during MCP exploration
    await page.goto('https://shop.example.com');
    await page.click('[data-testid="checkout-btn"]');

    // Should show error, not just disable button
    await expect(page.locator('.error')).toContainText('Cart is empty');
  });

  test('accepts Canadian postal codes', async ({ page }) => {
    // Bug found during MCP exploration
    await page.goto('https://shop.example.com/checkout');
    await page.fill('[name="postalCode"]', 'H3Z 2Y7');
    await page.blur('[name="postalCode"]');

    // Should not show validation error
    await expect(page.locator('.field-error')).not.toBeVisible();
  });

  test('shows clear success message after order', async ({ page }) => {
    // Enhancement from MCP exploration
    await page.goto('https://shop.example.com');

    // Complete checkout flow
    await page.click('[data-testid="add-to-cart"]');
    await page.click('[data-testid="checkout-btn"]');
    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="cardNumber"]', '4242424242424242');
    await page.click('button[type="submit"]');

    // Verify clear success
    await expect(page.locator('.success-message')).toContainText('Order confirmed');
    await expect(page.locator('.order-number')).toBeVisible();
  });
});

Phase 3: CI/CD Integration

# .github/workflows/test.yml
name: E2E Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/

Workflow Summary

MCP Exploration → Found 3 Issues → Wrote 3 Tests → CI/CD Catches Regressions

Before: Manual testing, issues slip through
After: Automated protection, bugs caught early

Time investment:

  • MCP exploration: 10 minutes (found 3 bugs)
  • Writing tests: 15 minutes (permanent protection)
  • CI/CD setup: 5 minutes (one-time)
  • Total: 30 minutes for permanent regression protection

Value:

  • Bugs found: 3 (before users saw them)
  • Future regressions prevented: ∞
  • Developer confidence: 📈

When to Skip Permanent Tests

Sometimes MCP exploration is enough:

Write permanent tests for:

  • Critical user flows (login, checkout, signup)
  • Frequently changing features
  • Bug-prone areas
  • Compliance requirements

MCP exploration only for:

  • One-time audits
  • Prototype testing
  • Features scheduled for removal
  • Quick sanity checks

The Iron Law (Reminder): Explore first with MCP, then lock it in with tests. Never write tests blindly without understanding the actual user flow.

Running Tests

# Run all tests
npx playwright test

# Run specific test file
npx playwright test tests/login.spec.ts

# Run in headed mode (see browser)
npx playwright test --headed

# Run in specific browser
npx playwright test --project=chromium

# Run with UI mode (interactive)
npx playwright test --ui

# Debug mode
npx playwright test --debug

# Generate code (record actions)
npx playwright codegen https://example.com

Integration with Database Backup Skill

CRITICAL: When testing involves database operations:

import { test } from '@playwright/test';
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

test.beforeEach(async () => {
  // Use database-backup skill before tests
  await execAsync('./scripts/backup-database.sh');
});

test('user registration', async ({ page }) => {
  // Test that modifies database
  await page.goto('/register');
  // ... registration flow
});

Integration with TDD Skill

Follow RED-GREEN-REFACTOR:

// 🔴 RED: Write failing test first
test('user can add todo item', async ({ page }) => {
  await page.goto('/todos');
  await page.fill('input[name="todo"]', 'Buy groceries');
  await page.click('button[type="submit"]');

  await expect(page.locator('.todo-item')).toContainText('Buy groceries');
});

// Run test: ❌ FAILS (feature doesn't exist yet)

// 🟢 GREEN: Implement minimal code to pass
// [Implement todo addition feature]

// Run test: ✅ PASSES

// 🔵 REFACTOR: Improve code while keeping test green
// [Refactor todo code]

// Run test: ✅ STILL PASSES

Docker Support

# Dockerfile
FROM mcr.microsoft.com/playwright:v1.40.0-focal

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npx", "playwright", "test"]
# Build and run
docker build -t my-playwright-tests .
docker run my-playwright-tests

CI/CD Integration

GitHub Actions

# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [ main ]
  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: Backup database (if needed)
        run: ./scripts/backup-database.sh

      - name: Run Playwright tests
        run: npx playwright test

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: playwright-report/

GitLab CI

# .gitlab-ci.yml
playwright:
  image: mcr.microsoft.com/playwright:v1.40.0-focal
  script:
    - npm ci
    - npx playwright test
  artifacts:
    when: always
    paths:
      - playwright-report/
    reports:
      junit: test-results/junit.xml

Common Mistakes

Mistake 1: Not Waiting for Elements

// ❌ Bad: Clicking before element is ready
await page.click('button');

// ✅ Good: Playwright auto-waits
await page.click('button'); // Actually waits automatically

// ✅ Better: Explicit wait if needed
await page.waitForSelector('button', { state: 'visible' });
await page.click('button');

Mistake 2: Using Brittle Selectors

// ❌ Bad: Implementation details
await page.click('.MuiButton-root.MuiButton-containedPrimary');

// ✅ Good: Semantic selectors
await page.click('role=button[name="Submit"]');
await page.click('[data-testid="submit-btn"]');

Mistake 3: No Test Isolation

// ❌ Bad: Tests depend on each other
test('create user', async ({ page }) => {
  // Creates user...
});

test('login user', async ({ page }) => {
  // Assumes user from previous test exists
});

// ✅ Good: Each test is independent
test('login user', async ({ page }) => {
  // Create user in this test or use fixtures
  await createTestUser();
  // Login...
});

Mistake 4: Ignoring Network State

// ❌ Bad: Not waiting for API
await page.click('button');
// Immediately check result (API might not have returned)

// ✅ Good: Wait for network
await Promise.all([
  page.waitForResponse('**/api/submit'),
  page.click('button')
]);

Integration with Other Skills

Use with:

  • test-driven-development - Write Playwright tests first (RED-GREEN-REFACTOR)
  • condition-based-waiting - Wait for conditions, not arbitrary timeouts
  • database-backup - ALWAYS backup before tests that modify database
  • systematic-debugging - Debug failing Playwright tests methodically
  • code-review - Review test code for quality and coverage

Complements:

  • testing-anti-patterns - Avoid common testing mistakes
  • verification-before-completion - Verify all tests pass before declaring done

Checklist

Before running Playwright tests:

  • Playwright MCP configured (if using AI assistance)
  • Playwright installed in project
  • Tests use semantic selectors
  • Tests are isolated (no dependencies)
  • Database backup before tests (if DB involved)
  • Tests wait for conditions (not timeouts)
  • Tests handle network/async properly

Authority

This skill is based on:

  • Playwright official documentation
  • Microsoft's Playwright MCP implementation
  • Frontend testing best practices
  • Accessibility-first testing approach
  • Model Context Protocol standard

Social Proof: Playwright is used by Microsoft, Google, and thousands of companies for reliable frontend testing.

Your Commitment

When writing Playwright tests:

  • I will use semantic, accessible selectors
  • I will wait for conditions, not timeouts
  • I will keep tests isolated and independent
  • I will backup database before tests (if applicable)
  • I will follow TDD: test first, then implement
  • I will use Playwright MCP for AI-assisted testing

Bottom Line: Playwright with MCP enables fast, deterministic, AI-assisted frontend testing through accessibility trees. Use semantic selectors, wait for conditions, keep tests isolated, and always backup database before tests that modify data.