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>
390 lines
9.5 KiB
Markdown
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);
|
|
```
|