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>
This commit is contained in:
2026-01-20 15:47:34 -05:00
commit befb8fbaeb
34 changed files with 12233 additions and 0 deletions

View File

@@ -0,0 +1,389 @@
---
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);
```

View File

@@ -0,0 +1,349 @@
---
name: tdd-workflow
description: Test-Driven Development workflow with RED-GREEN-REFACTOR cycle, coverage verification, and quality gates. Use when planning or implementing features with TDD.
---
# TDD Workflow Skill
## The TDD Cycle
```
┌─────────────────────────────────────────────────────┐
│ │
│ ┌─────┐ ┌───────┐ ┌──────────┐ │
│ │ RED │ ──▶ │ GREEN │ ──▶ │ REFACTOR │ ──┐ │
│ └─────┘ └───────┘ └──────────┘ │ │
│ ▲ │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
```
## Phase 1: RED (Write Failing Test)
### Rules
- Write ONE test that describes desired behavior
- Test must fail for the RIGHT reason
- NO production code exists yet
### Checklist
- [ ] Test describes behavior, not implementation
- [ ] Test name clearly states expected outcome
- [ ] Test uses factory functions for data
- [ ] Test fails with meaningful error message
- [ ] Error indicates missing behavior, not syntax/import errors
### Example
```typescript
// STEP 1: Write the failing test
describe('PaymentProcessor', () => {
it('should reject payments with negative amounts', () => {
const payment = getMockPayment({ amount: -100 });
const result = processPayment(payment);
expect(result.success).toBe(false);
expect(result.error).toBe('Amount must be positive');
});
});
// At this point:
// - processPayment doesn't exist
// - Running test gives: "ReferenceError: processPayment is not defined"
// - This is the RIGHT failure - the function doesn't exist yet
```
## Phase 2: GREEN (Write Minimum Code)
### Rules
- Write ONLY enough code to pass the test
- No additional functionality
- No "while you're there" improvements
- Ugly code is acceptable (we'll refactor)
### Checklist
- [ ] Implementation makes test pass
- [ ] No code beyond what test demands
- [ ] All existing tests still pass
- [ ] Coverage increased for new code
### Example
```typescript
// STEP 2: Write minimum code to pass
export function processPayment(payment: Payment): PaymentResult {
if (payment.amount < 0) {
return { success: false, error: 'Amount must be positive' };
}
return { success: true };
}
// This passes the test - that's enough for now
// Don't add validation for zero, max amounts, etc.
// Those need their own tests first!
```
## Phase 3: REFACTOR (Improve Code Quality)
### Rules
- COMMIT before refactoring
- Only refactor if it adds value
- All tests must stay green
- No new functionality
### When to Refactor
- Duplicate knowledge (not just similar code)
- Unclear names
- Complex conditionals
- Magic numbers/strings
### When NOT to Refactor
- Code is already clean
- Similarity is structural, not semantic
- Would add unnecessary abstraction
### Example
```typescript
// STEP 3: Assess and refactor
// BEFORE REFACTORING: git commit -m "feat: add negative amount validation"
// Assessment:
// - Magic number 0 could be a constant
// - Error message could be a constant
// - But... it's simple enough. Skip refactoring.
// If we had multiple validations:
const VALIDATION_ERRORS = {
NEGATIVE_AMOUNT: 'Amount must be positive',
ZERO_AMOUNT: 'Amount must be greater than zero',
EXCEEDS_LIMIT: 'Amount exceeds maximum limit',
} as const;
const MAX_PAYMENT_AMOUNT = 10000;
export function processPayment(payment: Payment): PaymentResult {
if (payment.amount < 0) {
return { success: false, error: VALIDATION_ERRORS.NEGATIVE_AMOUNT };
}
if (payment.amount === 0) {
return { success: false, error: VALIDATION_ERRORS.ZERO_AMOUNT };
}
if (payment.amount > MAX_PAYMENT_AMOUNT) {
return { success: false, error: VALIDATION_ERRORS.EXCEEDS_LIMIT };
}
return { success: true };
}
```
## Complete TDD Session Example
```typescript
// === CYCLE 1: Negative amounts ===
// RED: Write failing test
it('should reject negative amounts', () => {
const payment = getMockPayment({ amount: -100 });
const result = processPayment(payment);
expect(result.success).toBe(false);
});
// RUN: ❌ FAIL - processPayment is not defined
// GREEN: Minimal implementation
function processPayment(payment: Payment): PaymentResult {
if (payment.amount < 0) return { success: false };
return { success: true };
}
// RUN: ✅ PASS
// REFACTOR: Assess - simple enough, skip
// COMMIT: "feat: reject negative payment amounts"
// === CYCLE 2: Zero amounts ===
// RED: Write failing test
it('should reject zero amounts', () => {
const payment = getMockPayment({ amount: 0 });
const result = processPayment(payment);
expect(result.success).toBe(false);
});
// RUN: ❌ FAIL - expected false, got true
// GREEN: Add zero check
function processPayment(payment: Payment): PaymentResult {
if (payment.amount <= 0) return { success: false };
return { success: true };
}
// RUN: ✅ PASS
// REFACTOR: Assess - still simple, skip
// COMMIT: "feat: reject zero payment amounts"
// === CYCLE 3: Maximum amount ===
// RED: Write failing test
it('should reject amounts over 10000', () => {
const payment = getMockPayment({ amount: 10001 });
const result = processPayment(payment);
expect(result.success).toBe(false);
});
// RUN: ❌ FAIL - expected false, got true
// GREEN: Add max check
function processPayment(payment: Payment): PaymentResult {
if (payment.amount <= 0) return { success: false };
if (payment.amount > 10000) return { success: false };
return { success: true };
}
// RUN: ✅ PASS
// REFACTOR: Now we have magic number 10000
// COMMIT first: "feat: reject payments over maximum"
// Then refactor:
const MAX_PAYMENT_AMOUNT = 10000;
function processPayment(payment: Payment): PaymentResult {
if (payment.amount <= 0) return { success: false };
if (payment.amount > MAX_PAYMENT_AMOUNT) return { success: false };
return { success: true };
}
// RUN: ✅ PASS
// COMMIT: "refactor: extract max payment constant"
```
## Quality Gates
Before committing, verify:
```bash
# 1. All tests pass
npm test # or pytest, cargo test
# 2. Coverage meets threshold
npm test -- --coverage
# Verify: 80%+ overall
# 3. Type checking passes
npm run typecheck # or mypy, cargo check
# 4. Linting passes
npm run lint # or ruff, cargo clippy
# 5. No secrets in code
git diff --staged | grep -i "password\|secret\|api.key"
```
## Coverage Verification
**CRITICAL: Never trust claimed coverage - always verify.**
```bash
# Python
pytest --cov=src --cov-report=term-missing --cov-fail-under=80
# TypeScript
npm test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80}}'
# Rust
cargo tarpaulin --fail-under 80
```
### Interpreting Coverage
| Metric | Meaning | Target |
|--------|---------|--------|
| Lines | % of lines executed | 80%+ |
| Branches | % of if/else paths taken | 80%+ |
| Functions | % of functions called | 80%+ |
| Statements | % of statements executed | 80%+ |
### Coverage Exceptions (Document!)
```python
# pragma: no cover - Reason: Debug utility only used in development
def debug_print(data):
print(json.dumps(data, indent=2))
```
```typescript
/* istanbul ignore next -- @preserve Reason: Error boundary for React */
if (process.env.NODE_ENV === 'development') {
console.error(error);
}
```
## Anti-Patterns
### Writing Test After Code
```
❌ WRONG:
1. Write all production code
2. Write tests to cover it
3. "I have 80% coverage!"
✅ CORRECT:
1. Write failing test
2. Write code to pass
3. Repeat until feature complete
```
### Testing Implementation
```typescript
// ❌ BAD: Tests implementation detail
it('should call validatePayment', () => {
const spy = jest.spyOn(service, 'validatePayment');
processPayment(payment);
expect(spy).toHaveBeenCalled();
});
// ✅ GOOD: Tests behavior
it('should reject invalid payments', () => {
const payment = getMockPayment({ amount: -1 });
const result = processPayment(payment);
expect(result.success).toBe(false);
});
```
### Writing Too Many Tests at Once
```
❌ WRONG:
1. Write 10 tests
2. Implement everything
3. All tests pass
✅ CORRECT:
1. Write 1 test
2. Make it pass
3. Refactor if needed
4. Repeat
```
### Skipping Refactor Assessment
```
❌ WRONG:
1. Test passes
2. Immediately write next test
3. Code becomes messy
✅ CORRECT:
1. Test passes
2. Ask: "Should I refactor?"
3. If yes: commit, then refactor
4. If no: continue
```
## Commit Messages
Follow this pattern:
```bash
# After GREEN (feature added)
git commit -m "feat: add payment amount validation"
# After REFACTOR
git commit -m "refactor: extract payment validation constants"
# Test-only changes
git commit -m "test: add edge cases for payment validation"
# Bug fixes (driven by failing test)
git commit -m "fix: handle null payment amount"
```

View File

@@ -0,0 +1,445 @@
---
name: ui-testing
description: React component testing with React Testing Library, user-centric patterns, and accessibility testing. Use when writing tests for React components or UI interactions.
---
# UI Testing Skill
## Core Principles
1. **Test behavior, not implementation** - What users see and do
2. **Query by accessibility** - Role, label, text (what assistive tech sees)
3. **User events over fire events** - Real user interactions
4. **Avoid test IDs when possible** - Last resort only
## Query Priority
```typescript
// Best: Accessible to everyone
screen.getByRole('button', { name: 'Submit' })
screen.getByLabelText('Email address')
screen.getByPlaceholderText('Search...')
screen.getByText('Welcome back')
// Good: Semantic queries
screen.getByAltText('Company logo')
screen.getByTitle('Close dialog')
// Acceptable: Test IDs (when no other option)
screen.getByTestId('custom-datepicker')
```
## Component Testing Pattern
```typescript
// tests/features/dashboard/Dashboard.test.tsx
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { Dashboard } from '@/features/dashboard/Dashboard';
import { getMockUser, getMockProjects } from './factories';
describe('Dashboard', () => {
it('should display user name in header', () => {
const user = getMockUser({ name: 'Alice' });
render(<Dashboard user={user} projects={[]} />);
expect(
screen.getByRole('heading', { name: /welcome, alice/i })
).toBeInTheDocument();
});
it('should show empty state when no projects', () => {
render(<Dashboard user={getMockUser()} projects={[]} />);
expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /create project/i })
).toBeInTheDocument();
});
it('should list all projects with their status', () => {
const projects = [
getMockProject({ name: 'Project A', status: 'active' }),
getMockProject({ name: 'Project B', status: 'completed' }),
];
render(<Dashboard user={getMockUser()} projects={projects} />);
const projectList = screen.getByRole('list', { name: /projects/i });
const items = within(projectList).getAllByRole('listitem');
expect(items).toHaveLength(2);
expect(screen.getByText('Project A')).toBeInTheDocument();
expect(screen.getByText('Project B')).toBeInTheDocument();
});
it('should navigate to project when clicked', async () => {
const user = userEvent.setup();
const onNavigate = vi.fn();
const projects = [getMockProject({ id: 'proj-1', name: 'My Project' })];
render(
<Dashboard
user={getMockUser()}
projects={projects}
onNavigate={onNavigate}
/>
);
await user.click(screen.getByRole('link', { name: /my project/i }));
expect(onNavigate).toHaveBeenCalledWith('/projects/proj-1');
});
});
```
## Form Testing
```typescript
describe('LoginForm', () => {
it('should show validation errors for empty fields', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});
it('should show error for invalid email format', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
await user.type(screen.getByLabelText(/email/i), 'invalid-email');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(screen.getByText(/invalid email format/i)).toBeInTheDocument();
});
it('should submit form with valid data', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'user@example.com');
await user.type(screen.getByLabelText(/password/i), 'securepass123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'securepass123',
});
});
it('should disable submit button while loading', async () => {
render(<LoginForm onSubmit={vi.fn()} isLoading />);
expect(screen.getByRole('button', { name: /signing in/i })).toBeDisabled();
});
it('should show server error message', () => {
render(
<LoginForm onSubmit={vi.fn()} error="Invalid credentials" />
);
expect(screen.getByRole('alert')).toHaveTextContent(/invalid credentials/i);
});
});
```
## Async Testing
```typescript
describe('UserProfile', () => {
it('should show loading state initially', () => {
render(<UserProfile userId="user-1" />);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
it('should display user data when loaded', async () => {
// Mock API returns user data
server.use(
http.get('/api/users/user-1', () => {
return HttpResponse.json(getMockUser({ name: 'Alice' }));
})
);
render(<UserProfile userId="user-1" />);
// Wait for data to load
await waitFor(() => {
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});
expect(screen.getByText('Alice')).toBeInTheDocument();
});
it('should show error state on failure', async () => {
server.use(
http.get('/api/users/user-1', () => {
return HttpResponse.error();
})
);
render(<UserProfile userId="user-1" />);
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
});
expect(screen.getByText(/failed to load user/i)).toBeInTheDocument();
});
});
```
## Modal and Dialog Testing
```typescript
describe('ConfirmDialog', () => {
it('should not be visible when closed', () => {
render(<ConfirmDialog isOpen={false} onConfirm={vi.fn()} />);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('should be visible when open', () => {
render(
<ConfirmDialog
isOpen={true}
title="Delete item?"
message="This action cannot be undone."
onConfirm={vi.fn()}
/>
);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(
screen.getByRole('heading', { name: /delete item/i })
).toBeInTheDocument();
});
it('should call onConfirm when confirmed', async () => {
const user = userEvent.setup();
const onConfirm = vi.fn();
render(
<ConfirmDialog isOpen={true} onConfirm={onConfirm} onCancel={vi.fn()} />
);
await user.click(screen.getByRole('button', { name: /confirm/i }));
expect(onConfirm).toHaveBeenCalledTimes(1);
});
it('should call onCancel when cancelled', async () => {
const user = userEvent.setup();
const onCancel = vi.fn();
render(
<ConfirmDialog isOpen={true} onConfirm={vi.fn()} onCancel={onCancel} />
);
await user.click(screen.getByRole('button', { name: /cancel/i }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
it('should close on escape key', async () => {
const user = userEvent.setup();
const onCancel = vi.fn();
render(
<ConfirmDialog isOpen={true} onConfirm={vi.fn()} onCancel={onCancel} />
);
await user.keyboard('{Escape}');
expect(onCancel).toHaveBeenCalledTimes(1);
});
});
```
## Accessibility Testing
```typescript
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe('Accessibility', () => {
it('should have no accessibility violations', async () => {
const { container } = render(<LoginForm onSubmit={vi.fn()} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should have proper focus management', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
// Tab through form
await user.tab();
expect(screen.getByLabelText(/email/i)).toHaveFocus();
await user.tab();
expect(screen.getByLabelText(/password/i)).toHaveFocus();
await user.tab();
expect(screen.getByRole('button', { name: /sign in/i })).toHaveFocus();
});
it('should announce errors to screen readers', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
await user.click(screen.getByRole('button', { name: /sign in/i }));
const errorMessage = screen.getByText(/email is required/i);
expect(errorMessage).toHaveAttribute('role', 'alert');
});
});
```
## Testing Select/Dropdown Components
```typescript
describe('CountrySelect', () => {
it('should show selected country', () => {
render(<CountrySelect value="GB" onChange={vi.fn()} />);
expect(screen.getByRole('combobox')).toHaveTextContent('United Kingdom');
});
it('should show options when opened', async () => {
const user = userEvent.setup();
render(<CountrySelect value="" onChange={vi.fn()} />);
await user.click(screen.getByRole('combobox'));
expect(screen.getByRole('listbox')).toBeInTheDocument();
expect(screen.getByRole('option', { name: /united states/i })).toBeInTheDocument();
expect(screen.getByRole('option', { name: /united kingdom/i })).toBeInTheDocument();
});
it('should call onChange when option selected', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
render(<CountrySelect value="" onChange={onChange} />);
await user.click(screen.getByRole('combobox'));
await user.click(screen.getByRole('option', { name: /germany/i }));
expect(onChange).toHaveBeenCalledWith('DE');
});
it('should filter options when typing', async () => {
const user = userEvent.setup();
render(<CountrySelect value="" onChange={vi.fn()} />);
await user.click(screen.getByRole('combobox'));
await user.type(screen.getByRole('combobox'), 'united');
const options = screen.getAllByRole('option');
expect(options).toHaveLength(2); // US and UK
});
});
```
## Testing with Context Providers
```typescript
// tests/utils/render.tsx
import { render, type RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from '@/shared/context/ThemeContext';
import { AuthProvider } from '@/features/auth/AuthContext';
type WrapperProps = {
children: React.ReactNode;
};
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
export function renderWithProviders(
ui: React.ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) {
const queryClient = createTestQueryClient();
function Wrapper({ children }: WrapperProps) {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<ThemeProvider>{children}</ThemeProvider>
</AuthProvider>
</QueryClientProvider>
);
}
return {
...render(ui, { wrapper: Wrapper, ...options }),
queryClient,
};
}
```
## Anti-Patterns to Avoid
```typescript
// BAD: Testing implementation details
it('should update state', () => {
const { result } = renderHook(() => useState(0));
act(() => result.current[1](1));
expect(result.current[0]).toBe(1);
});
// GOOD: Test user-visible behavior
it('should increment counter when clicked', async () => {
render(<Counter />);
await userEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('1')).toBeInTheDocument();
});
// BAD: Using test IDs when accessible query exists
screen.getByTestId('submit-button')
// GOOD: Use accessible query
screen.getByRole('button', { name: /submit/i })
// BAD: Waiting with arbitrary timeout
await new Promise(r => setTimeout(r, 1000));
// GOOD: Wait for specific condition
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});
// BAD: Using fireEvent for user interactions
fireEvent.click(button);
// GOOD: Use userEvent for realistic interactions
await userEvent.click(button);
// BAD: Querying by class or internal structure
container.querySelector('.btn-primary')
// GOOD: Query by role and accessible name
screen.getByRole('button', { name: /save/i })
```