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,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 })
```