Marketplace react-testing
Testing patterns for React with Jest and React Testing Library. Use when writing tests, mocking modules, testing Zustand stores, or debugging test failures in React web applications.
install
source · Clone the upstream repo
git clone https://github.com/aiskillstore/marketplace
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/cjharmath/react-testing" ~/.claude/skills/aiskillstore-marketplace-react-testing && rm -rf "$T"
manifest:
skills/cjharmath/react-testing/SKILL.mdsource content
React Testing (Web)
Problem Statement
React testing requires understanding component rendering, user interactions, and async state management. This skill covers Jest with React Testing Library patterns for web applications.
Pattern: Zustand Store Testing
Problem: Store state persists between tests, causing flaky tests.
import { useAppStore } from '@/stores/appStore'; const initialState = { items: [], loading: false, error: null, }; describe('App Store', () => { // Reset store before each test beforeEach(() => { useAppStore.setState(initialState, true); // true = replace entire state }); it('adds item to store', async () => { const store = useAppStore.getState(); await store.addItem({ id: '1', name: 'Test' }); expect(useAppStore.getState().items).toHaveLength(1); }); it('handles loading state', async () => { const store = useAppStore.getState(); const loadPromise = store.fetchItems(); expect(useAppStore.getState().loading).toBe(true); await loadPromise; expect(useAppStore.getState().loading).toBe(false); }); });
Key points:
- Use
to replace (not merge) statesetState(initialState, true) - Get fresh state with
after async operationsgetState() - Don't rely on component re-renders in store tests
Pattern: Async Store Operations
Problem: Testing async Zustand actions with proper waiting.
import { act, waitFor } from '@testing-library/react'; it('loads data correctly', async () => { const store = useAppStore.getState(); // Wrap async store operations in act await act(async () => { await store.loadData('123'); }); // Verify state after async completes await waitFor(() => { const state = useAppStore.getState(); expect(Object.keys(state.data).length).toBeGreaterThan(0); }); }); // For complex flows, verify each step it('completes multi-step flow', async () => { const store = useAppStore.getState(); // Step 1 await act(async () => { await store.loadItems(); }); expect(useAppStore.getState().items).toBeDefined(); // Step 2 await act(async () => { await store.processItems(); }); expect(useAppStore.getState().processed).toBe(true); });
Pattern: Component Testing
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; describe('ItemCard', () => { const mockItem = { id: '1', title: 'Test Item', price: 99.99, }; it('displays item data', () => { render(<ItemCard item={mockItem} />); expect(screen.getByText('Test Item')).toBeInTheDocument(); expect(screen.getByText('$99.99')).toBeInTheDocument(); }); it('calls onClick when clicked', async () => { const user = userEvent.setup(); const onClick = jest.fn(); render(<ItemCard item={mockItem} onClick={onClick} />); await user.click(screen.getByRole('button')); expect(onClick).toHaveBeenCalledWith(mockItem.id); }); it('shows loading state', () => { render(<ItemCard item={mockItem} loading />); expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); }); });
Pattern: React Query Testing
Problem: Components using React Query need QueryClientProvider.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen, waitFor } from '@testing-library/react'; function createTestQueryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0, }, }, }); } function renderWithQuery(ui: React.ReactElement) { const queryClient = createTestQueryClient(); return render( <QueryClientProvider client={queryClient}> {ui} </QueryClientProvider> ); } // Usage in tests it('fetches and displays data', async () => { renderWithQuery(<UserProfile userId="123" />); // Shows loading initially expect(screen.getByText('Loading...')).toBeInTheDocument(); // Wait for data await waitFor(() => { expect(screen.getByText('John Doe')).toBeInTheDocument(); }); });
Pattern: Custom Hook Testing
import { renderHook, act, waitFor } from '@testing-library/react'; describe('useAuth', () => { it('signs in user', async () => { const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, // If hook needs context }); await act(async () => { await result.current.signIn('test@example.com', 'password'); }); expect(result.current.user).toBeDefined(); expect(result.current.isAuthenticated).toBe(true); }); it('handles sign in error', async () => { const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await act(async () => { try { await result.current.signIn('invalid@example.com', 'wrong'); } catch (e) { // Expected } }); expect(result.current.error).toBe('Invalid credentials'); }); }); // Hook with Zustand describe('useUserData', () => { beforeEach(() => { useUserStore.setState(initialState, true); }); it('returns current user data', () => { // Pre-populate store useUserStore.setState({ user: { id: '1', name: 'Test' } }); const { result } = renderHook(() => useUserData()); expect(result.current.user.name).toBe('Test'); }); });
Pattern: Mocking API Calls
// Mock fetch globally global.fetch = jest.fn(); beforeEach(() => { (fetch as jest.Mock).mockClear(); }); it('fetches user data', async () => { (fetch as jest.Mock).mockResolvedValueOnce({ ok: true, json: async () => ({ id: '1', name: 'John' }), }); render(<UserProfile userId="1" />); await waitFor(() => { expect(screen.getByText('John')).toBeInTheDocument(); }); expect(fetch).toHaveBeenCalledWith('/api/users/1'); }); // Mock specific module jest.mock('@/api/users', () => ({ getUser: jest.fn(), updateUser: jest.fn(), })); import { getUser, updateUser } from '@/api/users'; it('loads and updates user', async () => { (getUser as jest.Mock).mockResolvedValue({ id: '1', name: 'John' }); (updateUser as jest.Mock).mockResolvedValue({ id: '1', name: 'Jane' }); // Test component that uses these });
Pattern: Router Testing
import { MemoryRouter, Routes, Route } from 'react-router-dom'; function renderWithRouter(ui: React.ReactElement, { route = '/' } = {}) { return render( <MemoryRouter initialEntries={[route]}> {ui} </MemoryRouter> ); } // Test navigation it('navigates to profile on button click', async () => { const user = userEvent.setup(); renderWithRouter( <Routes> <Route path="/" element={<HomePage />} /> <Route path="/profile" element={<ProfilePage />} /> </Routes> ); await user.click(screen.getByText('Go to Profile')); expect(screen.getByText('Profile Page')).toBeInTheDocument(); }); // Test with route params it('displays user from route params', async () => { renderWithRouter( <Routes> <Route path="/users/:id" element={<UserPage />} /> </Routes>, { route: '/users/123' } ); await waitFor(() => { expect(screen.getByText('User 123')).toBeInTheDocument(); }); });
Pattern: Form Testing
import userEvent from '@testing-library/user-event'; describe('LoginForm', () => { it('submits form with entered data', async () => { const user = userEvent.setup(); const onSubmit = jest.fn(); render(<LoginForm onSubmit={onSubmit} />); await user.type(screen.getByLabelText('Email'), 'test@example.com'); await user.type(screen.getByLabelText('Password'), 'password123'); await user.click(screen.getByRole('button', { name: 'Sign In' })); expect(onSubmit).toHaveBeenCalledWith({ email: 'test@example.com', password: 'password123', }); }); it('shows validation errors', async () => { const user = userEvent.setup(); render(<LoginForm onSubmit={jest.fn()} />); // Submit without filling form await user.click(screen.getByRole('button', { name: 'Sign In' })); expect(screen.getByText('Email is required')).toBeInTheDocument(); expect(screen.getByText('Password is required')).toBeInTheDocument(); }); it('disables submit while loading', async () => { render(<LoginForm onSubmit={jest.fn()} loading />); expect(screen.getByRole('button', { name: 'Sign In' })).toBeDisabled(); }); });
Pattern: Avoiding act() Warnings
Problem: "Warning: An update inside a test was not wrapped in act(...)"
// WRONG - state update happens after test it('loads data', () => { render(<DataComponent />); // Component fetches data async, updates state after test ends }); // CORRECT - wait for async completion it('loads data', async () => { render(<DataComponent />); // Wait for loading to complete await waitFor(() => { expect(screen.getByText('Data loaded')).toBeInTheDocument(); }); }); // CORRECT - use findBy* (has built-in waitFor) it('loads data', async () => { render(<DataComponent />); const element = await screen.findByText('Data loaded'); expect(element).toBeInTheDocument(); });
Pattern: Snapshot Testing
When to use:
- UI components with stable structure
- Design system components
- Components where visual regression matters
When to avoid:
- Components with dynamic content
- Components that change frequently
- Large component trees (brittle)
// Good snapshot candidate - stable UI component it('renders correctly', () => { const { container } = render(<Button variant="primary">Submit</Button>); expect(container).toMatchSnapshot(); }); // Bad snapshot candidate - dynamic content it('renders user list', () => { // Don't snapshot - list content varies // Instead, test specific behaviors });
Pattern: Testing Context Providers
// Create a wrapper with all providers function AllProviders({ children }: { children: React.ReactNode }) { const queryClient = createTestQueryClient(); return ( <QueryClientProvider client={queryClient}> <AuthProvider> <ThemeProvider> {children} </ThemeProvider> </AuthProvider> </QueryClientProvider> ); } function renderWithProviders(ui: React.ReactElement) { return render(ui, { wrapper: AllProviders }); } // Use in tests it('renders with all context', () => { renderWithProviders(<Dashboard />); // Component has access to all providers });
Test Commands
npm test # Run all tests npm test -- --watch # Watch mode npm test -- --coverage # Coverage report npm test -- Button # Run specific test file npm test -- --updateSnapshot # Update snapshots npm test -- --runInBand # Run tests serially (debugging)
Common Issues
| Issue | Solution |
|---|---|
| "Cannot find module" | Check jest moduleNameMapper config |
| act() warning | Wrap state updates in act(), use waitFor/findBy |
| Store state bleeding | Add beforeEach with setState reset |
| Async test timeout | Increase timeout or check for hanging promises |
| Mock not working | Verify mock path matches import path exactly |
| Query not found | Use findBy* for async content, check accessibility |
Recommended File Structure
__tests__/ utils/ test-utils.tsx # Custom render with providers query-test-utils.tsx # QueryClient wrapper jest.setup.js # Global mocks and setup jest.config.js # Jest configuration