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

390 lines
9.5 KiB
Markdown

---
name: browser-testing
description: 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```bash
# 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)
```typescript
page.getByRole('button', { name: 'Submit' })
page.getByLabel('Email')
```
2. **Wait for specific conditions** (not arbitrary timeouts)
```typescript
await expect(page.getByText('Success')).toBeVisible();
```
3. **Isolate tests** (each test sets up its own state)
```typescript
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
```
4. **Use Page Objects** (DRY, maintainable)
```typescript
const loginPage = new LoginPage(page);
await loginPage.login(email, password);
```
5. **Mock external APIs** (fast, reliable)
```typescript
await page.route('**/api/**', handler);
```