Files
James Bland befb8fbaeb feat: initial Claude Code configuration scaffold
Comprehensive Claude Code guidance system with:

- 5 agents: tdd-guardian, code-reviewer, security-scanner, refactor-scan, dependency-audit
- 18 skills covering languages (Python, TypeScript, Rust, Go, Java, C#),
  infrastructure (AWS, Azure, GCP, Terraform, Ansible, Docker/K8s, Database, CI/CD),
  testing (TDD, UI, Browser), and patterns (Monorepo, API Design, Observability)
- 3 hooks: secret detection, auto-formatting, TDD git pre-commit
- Strict TDD enforcement with 80%+ coverage requirements
- Multi-model strategy: Opus for planning, Sonnet for execution (opusplan)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 15:47:34 -05:00

9.5 KiB

name, description
name description
browser-testing Browser automation with Playwright for E2E tests and Claude's Chrome MCP for rapid development verification. Use when writing E2E tests or verifying UI during development.

Browser Testing Skill

Two Approaches

1. Claude Chrome MCP (Development Verification)

Quick, interactive testing during development to verify UI works before writing permanent tests.

2. Playwright (Permanent E2E Tests)

Automated, repeatable tests that run in CI/CD.


Claude Chrome MCP (Quick Verification)

Use Chrome MCP tools for rapid UI verification during development.

Basic Workflow

1. Navigate to page
2. Take screenshot to verify state
3. Find and interact with elements
4. Verify expected outcome
5. Record GIF for complex flows

Navigation

// Navigate to URL
mcp__claude-in-chrome__navigate({ url: "http://localhost:5173", tabId: 123 })

// Go back/forward
mcp__claude-in-chrome__navigate({ url: "back", tabId: 123 })

Finding Elements

// Find by natural language
mcp__claude-in-chrome__find({ query: "login button", tabId: 123 })
mcp__claude-in-chrome__find({ query: "email input field", tabId: 123 })
mcp__claude-in-chrome__find({ query: "product card containing organic", tabId: 123 })

Reading Page State

// Get accessibility tree (best for understanding structure)
mcp__claude-in-chrome__read_page({ tabId: 123, filter: "interactive" })

// Get text content (for articles/text-heavy pages)
mcp__claude-in-chrome__get_page_text({ tabId: 123 })

Interactions

// Click element by reference
mcp__claude-in-chrome__computer({
  action: "left_click",
  ref: "ref_42",
  tabId: 123
})

// Type text
mcp__claude-in-chrome__computer({
  action: "type",
  text: "user@example.com",
  tabId: 123
})

// Fill form input
mcp__claude-in-chrome__form_input({
  ref: "ref_15",
  value: "test@example.com",
  tabId: 123
})

// Press keys
mcp__claude-in-chrome__computer({
  action: "key",
  text: "Enter",
  tabId: 123
})

Screenshots and GIFs

// Take screenshot
mcp__claude-in-chrome__computer({ action: "screenshot", tabId: 123 })

// Record GIF for complex interaction
mcp__claude-in-chrome__gif_creator({ action: "start_recording", tabId: 123 })
// ... perform actions ...
mcp__claude-in-chrome__gif_creator({
  action: "export",
  download: true,
  filename: "login-flow.gif",
  tabId: 123
})

Verification Pattern

1. Navigate to the page
2. Screenshot to see current state
3. Find the element you want to interact with
4. Interact with it
5. Screenshot to verify the result
6. If complex flow, record a GIF

Playwright (Permanent E2E Tests)

Project Setup

// 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'],
    ['junit', { outputFile: 'results.xml' }],
  ],
  use: {
    baseURL: 'http://localhost:5173',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'mobile',
      use: { ...devices['iPhone 13'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI,
  },
});

Page Object Pattern

// e2e/pages/login.page.ts
import { Page, Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign in' });
    this.errorMessage = page.getByRole('alert');
  }

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

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async expectError(message: string) {
    await expect(this.errorMessage).toContainText(message);
  }

  async expectLoggedIn() {
    await expect(this.page).toHaveURL('/dashboard');
  }
}

Test Patterns

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

test.describe('Login', () => {
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test('should show validation errors for empty form', async () => {
    await loginPage.submitButton.click();

    await expect(loginPage.page.getByText('Email is required')).toBeVisible();
    await expect(loginPage.page.getByText('Password is required')).toBeVisible();
  });

  test('should show error for invalid credentials', async () => {
    await loginPage.login('wrong@example.com', 'wrongpassword');

    await loginPage.expectError('Invalid credentials');
  });

  test('should redirect to dashboard on successful login', async () => {
    await loginPage.login('user@example.com', 'correctpassword');

    await loginPage.expectLoggedIn();
    await expect(loginPage.page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  });

  test('should persist session after page reload', async ({ page }) => {
    await loginPage.login('user@example.com', 'correctpassword');
    await loginPage.expectLoggedIn();

    await page.reload();

    await expect(page).toHaveURL('/dashboard');
  });
});

API Mocking

// e2e/dashboard/projects.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Projects Dashboard', () => {
  test('should display projects from API', async ({ page }) => {
    // Mock API response
    await page.route('**/api/projects', async (route) => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: '1', name: 'Project A', status: 'active' },
          { id: '2', name: 'Project B', status: 'completed' },
        ]),
      });
    });

    await page.goto('/dashboard');

    await expect(page.getByText('Project A')).toBeVisible();
    await expect(page.getByText('Project B')).toBeVisible();
  });

  test('should show error state on API failure', async ({ page }) => {
    await page.route('**/api/projects', async (route) => {
      await route.fulfill({ status: 500 });
    });

    await page.goto('/dashboard');

    await expect(page.getByRole('alert')).toContainText('Failed to load projects');
  });
});

Visual Regression Testing

// e2e/visual/components.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Visual Regression', () => {
  test('login page matches snapshot', async ({ page }) => {
    await page.goto('/login');

    await expect(page).toHaveScreenshot('login-page.png');
  });

  test('dashboard matches snapshot', async ({ page }) => {
    // Setup authenticated state
    await page.goto('/dashboard');

    await expect(page).toHaveScreenshot('dashboard.png', {
      mask: [page.locator('.timestamp')], // Mask dynamic content
    });
  });
});

Accessibility Testing

// e2e/a11y/accessibility.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Accessibility', () => {
  test('login page should have no accessibility violations', async ({ page }) => {
    await page.goto('/login');

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

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

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

    const results = await new AxeBuilder({ page })
      .exclude('.third-party-widget') // Exclude third-party content
      .analyze();

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

Commands

# Playwright
npx playwright test                    # Run all tests
npx playwright test --project=chromium # Specific browser
npx playwright test login.spec.ts      # Specific file
npx playwright test --ui               # Interactive UI mode
npx playwright test --debug            # Debug mode
npx playwright show-report             # View HTML report
npx playwright codegen                 # Generate tests by recording

# Update snapshots
npx playwright test --update-snapshots

Best Practices

  1. Use role-based locators (accessible to all)

    page.getByRole('button', { name: 'Submit' })
    page.getByLabel('Email')
    
  2. Wait for specific conditions (not arbitrary timeouts)

    await expect(page.getByText('Success')).toBeVisible();
    
  3. Isolate tests (each test sets up its own state)

    test.beforeEach(async ({ page }) => {
      await page.goto('/login');
    });
    
  4. Use Page Objects (DRY, maintainable)

    const loginPage = new LoginPage(page);
    await loginPage.login(email, password);
    
  5. Mock external APIs (fast, reliable)

    await page.route('**/api/**', handler);