Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Standardized guidelines and patterns for Frontend React Testing Strategy.
.claude/skills/valec3-frontend-react-testing-strategy/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-16 | ✗→✓ | ▲ Improved | 68% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 84% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 57% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 106% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 70% | 0% |
Test behavior, not implementation:
typescript// ❌ Bad - testing implementation details expect(component.state.count).toBe(1); // ✅ Good - testing user-visible behavior expect(screen.getByText('Count: 1')).toBeInTheDocument();
typescriptimport { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Button } from './Button'; describe('Button', () => { it('renders with correct text', () => { render(<Button>Click me</Button>); expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument(); }); it('calls onClick when clicked', async () => { const handleClick = jest.fn(); const user = userEvent.setup(); render(<Button onClick={handleClick}>Click</Button>); await user.click(screen.getByRole('button')); expect(handleClick).toHaveBeenCalledTimes(1); }); it('is disabled when disabled prop is true', () => { render(<Button disabled>Click</Button>); expect(screen.getByRole('button')).toBeDisabled(); }); });
typescriptimport { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { LoginForm } from './LoginForm'; describe('LoginForm', () => { it('submits form with email and password', async () => { const onSubmit = jest.fn(); const user = userEvent.setup(); render(<LoginForm onSubmit={onSubmit} />); // Fill form await user.type(screen.getByLabelText(/email/i), 'user@example.com'); await user.type(screen.getByLabelText(/password/i), 'password123'); // Submit await user.click(screen.getByRole('button', { name: /log in/i })); // Assert expect(onSubmit).toHaveBeenCalledWith({ email: 'user@example.com', password: 'password123' }); }); it('shows validation error for invalid email', async () => { const user = userEvent.setup(); render(<LoginForm onSubmit={jest.fn()} />); await user.type(screen.getByLabelText(/email/i), 'invalid'); await user.click(screen.getByRole('button', { name: /log in/i })); expect(await screen.findByText(/invalid email/i)).toBeInTheDocument(); }); });
typescriptimport { render, screen, waitFor } from '@testing-library/react'; import { UserProfile } from './UserProfile'; // Mock fetch global.fetch = jest.fn(); describe('UserProfile', () => { beforeEach(() => { (fetch as jest.Mock).mockClear(); }); it('displays user data after loading', async () => { (fetch as jest.Mock).mockResolvedValueOnce({ json: async () => ({ name: 'John Doe', email: 'john@example.com' }) }); render(<UserProfile userId="123" />); // Initially loading expect(screen.getByText(/loading/i)).toBeInTheDocument(); // Wait for data await waitFor(() => { expect(screen.getByText('John Doe')).toBeInTheDocument(); }); expect(screen.getByText('john@example.com')).toBeInTheDocument(); }); it('displays error message on fetch failure', async () => { (fetch as jest.Mock).mockRejectedValueOnce(new Error('Failed')); render(<UserProfile userId="123" />); expect(await screen.findByText(/error/i)).toBeInTheDocument(); }); });
typescriptimport { renderHook, waitFor } from '@testing-library/react'; import { useUser } from './useUser'; // Mock API jest.mock('./api', () => ({ fetchUser: jest.fn() })); import { fetchUser } from './api'; describe('useUser', () => { it('fetches user data', async () => { (fetchUser as jest.Mock).mockResolvedValue({ id: '1', name: 'John' }); const { result } = renderHook(() => useUser('1')); expect(result.current.isLoading).toBe(true); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.data).toEqual({ id: '1', name: 'John' }); }); it('refetches data when called', async () => { (fetchUser as jest.Mock).mockResolvedValue({ id: '1', name: 'John' }); const { result } = renderHook(() => useUser('1')); await waitFor(() => { expect(result.current.data).toBeTruthy(); }); (fetchUser as jest.Mock).mockResolvedValue({ id: '1', name: 'Jane' }); result.current.refetch(); await waitFor(() => { expect(result.current.data?.name).toBe('Jane'); }); }); });
typescriptimport { render, screen } from '@testing-library/react'; import { AuthContext } from './AuthContext'; import { ProtectedRoute } from './ProtectedRoute'; const renderWithAuth = (component: React.ReactElement, isAuthenticated = false) => { return render( <AuthContext.Provider value={{ isAuthenticated, user: null }}> {component} </AuthContext.Provider> ); }; describe('ProtectedRoute', () => { it('shows content when authenticated', () => { renderWithAuth(<ProtectedRoute>Secret</ProtectedRoute>, true); expect(screen.getByText('Secret')).toBeInTheDocument(); }); it('redirects when not authenticated', () => { renderWithAuth(<ProtectedRoute>Secret</ProtectedRoute>, false); expect(screen.getByText(/login/i)).toBeInTheDocument(); }); });
typescript// Priority order (use the highest you can): // 1. getByRole expect(screen.getByRole('button', { name: /submit/i })); // 2. getByLabelText (forms) expect(screen.getByLabelText(/email/i)); // 3. getByPlaceholderText expect(screen.getByPlaceholderText(/enter email/i)); // 4. getByText expect(screen.getByText(/welcome/i)); // 5. getByTestId (last resort) expect(screen.getByTestId('custom-element'));
typescriptimport { render } from '@testing-library/react'; import { Card } from './Card'; it('matches snapshot', () => { const { container } = render( <Card title="Test" description="Description" /> ); expect(container).toMatchSnapshot(); });
userEvent over fireEvent| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 11,863 | 8,700 | -27% | 1 | 1 | 0% | 1,936 | 3,560 | +84% | 0 | 0 | — |
case-02 | pass→pass | 14,985 | 13,112 | -12% | 1 | 1 | 0% | 2,785 | 4,376 | +57% | 0 | 0 | — |
case-03 | pass→pass | 6,812 | 4,358 | -36% | 1 | 1 | 0% | 1,280 | 2,635 | +106% | 0 | 0 | — |
case-04 | pass→pass | 11,592 | 10,074 | -13% | 1 | 1 | 0% | 2,152 | 3,648 | +70% | 0 | 0 | — |
case-05 | pass→pass | 8,796 | 5,817 | -34% | 1 | 1 | 0% | 1,766 | 2,765 | +57% | 0 | 0 | — |
case-06 | pass→pass | 9,498 | 6,703 | -29% | 1 | 1 | 0% | 1,474 | 3,043 | +106% | 0 | 0 | — |
case-07 | pass→pass | 6,668 | 4,301 | -35% | 1 | 1 | 0% | 1,131 | 2,609 | +131% | 0 | 0 | — |
case-08 | pass→pass | 9,782 | 3,611 | -63% | 1 | 1 | 0% | 1,764 | 2,567 | +46% | 0 | 0 | — |
case-09 | pass→pass | 4,947 | 4,716 | -5% | 1 | 1 | 0% | 900 | 2,612 | +190% | 0 | 0 | — |
case-10 | pass→pass | 10,683 | 6,941 | -35% | 1 | 1 | 0% | 1,998 | 3,099 | +55% | 0 | 0 | — |
case-11 | pass→pass | 9,592 | 6,280 | -35% | 1 | 1 | 0% | 1,780 | 2,944 | +65% | 0 | 0 | — |
case-12 | pass→pass | 8,402 | 7,767 | -8% | 1 | 1 | 0% | 1,659 | 3,290 | +98% | 0 | 0 | — |
case-13 | pass→pass | 10,933 | 6,865 | -37% | 1 | 1 | 0% | 2,094 | 3,334 | +59% | 0 | 0 | — |
case-14 | pass→pass | 10,984 | 5,946 | -46% | 1 | 1 | 0% | 1,853 | 3,130 | +69% | 0 | 0 | — |
case-15 | pass→pass | 4,522 | 4,782 | +6% | 1 | 1 | 0% | 794 | 2,707 | +241% | 0 | 0 | — |
case-16 | fail→pass | 11,077 | 9,909 | -11% | 1 | 1 | 0% | 2,294 | 3,864 | +68% | 0 | 0 | — |
case-17 | fail→fail | 11,397 | 7,547 | -34% | 1 | 1 | 0% | 2,338 | 2,978 | +27% | 0 | 0 | — |
case-18 | pass→pass | 11,892 | 7,809 | -34% | 1 | 1 | 0% | 1,987 | 3,209 | +61% | 0 | 0 | — |
case-19 | pass→pass | 5,160 | 2,895 | -44% | 1 | 1 | 0% | 782 | 2,413 | +209% | 0 | 0 | — |
case-20 | pass→pass | 12,232 | 4,357 | -64% | 1 | 1 | 0% | 2,103 | 2,589 | +23% | 0 | 0 | — |
case-21 | pass→pass | 8,684 | 6,871 | -21% | 1 | 1 | 0% | 1,619 | 3,277 | +102% | 0 | 0 | — |
case-22 | pass→pass | 12,196 | 8,141 | -33% | 1 | 1 | 0% | 2,698 | 3,383 | +25% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.