| name | react-testing-patterns |
| description | Guides expert-level react testing patterns implementation: typescript and testing decision frameworks, production-ready patterns, and concrete templates for react testing patterns workflows.
Use when the user asks about react testing patterns, react testing patterns configuration, or typescript best practices for react projects.
Do NOT use when the user needs a different web development capability -- check sibling skills in the web development subcategory.
|
| license | Apache-2.0 |
| metadata | {"author":"foundry-skills","version":"1.0.0","tags":"typescript testing frameworks accessibility","category":"web-development","subcategory":"web-development","depends":"","disclaimer":"none","difficulty":"intermediate"} |
React Testing Patterns
When to Use
Use this skill when:
- The user asks how to test a specific React component pattern -- hooks, context providers, compound components, portals, or async data fetching
- The user wants to choose between testing approaches: React Testing Library vs Enzyme vs Cypress component testing vs Vitest
- The user is writing TypeScript-typed tests and needs guidance on typing mocks, spy functions, or render helpers
- The user wants to add accessibility assertions to their test suite using jest-axe, @testing-library/jest-dom, or aria role queries
- The user needs to test components that depend on routing (React Router), global state (Redux, Zustand, Jotai), or server state (React Query, SWR)
- The user is setting up a test configuration from scratch -- jest.config.ts, vitest.config.ts, setupTests.ts, or MSW handlers
- The user asks about testing coverage thresholds, CI integration, or test performance (suite taking over 60 seconds)
- The user needs to test error boundaries, Suspense boundaries, or React 18 concurrent rendering behavior
- The user wants patterns for testing custom hooks in isolation without rendering a full component tree
- The user asks about snapshot testing -- when to use it, when to avoid it, and how to maintain snapshots properly
Do NOT use this skill when:
- The user needs help with end-to-end testing of full user flows across multiple pages -- use a dedicated E2E skill covering Playwright or Cypress test authoring
- The user is asking about backend API testing, Node.js unit testing, or Express middleware testing -- those have different tool sets
- The user is asking about performance profiling or React DevTools -- that is a separate profiling skill
- The user needs help with React component architecture or design patterns unrelated to testing -- check the React architecture skill
- The user is building a non-React frontend (Vue, Svelte, Angular, Lit) -- testing patterns differ meaningfully enough to require a different skill
- The user only needs to configure CI/CD pipelines generically -- the testing patterns here assume tests are already written
- The user is asking about visual regression testing with tools like Chromatic or Percy -- those require snapshot diffing workflows not covered here
Process
1. Establish the Testing Stack and Configuration
Before writing a single test, confirm the foundational toolchain and configuration:
- Choose the test runner: Vitest is the recommended default for Vite-based projects (React + Vite, Next.js with custom Vite config). Jest remains the default for Create React App and Next.js projects not using Vite. Vitest offers 2-5x faster cold starts due to native ESM and shared Vite transform pipeline.
- Set up React Testing Library (RTL): Install
@testing-library/react, @testing-library/user-event (v14+, which uses async pointer events by default), and @testing-library/jest-dom. RTL's render is the primary entry point for all component tests.
- Configure jsdom: Both Jest and Vitest need a DOM environment. Set
testEnvironment: 'jsdom' in jest.config.ts or environment: 'jsdom' in vitest.config.ts. Add setupFilesAfterFramework: ['./src/setupTests.ts'] to auto-import @testing-library/jest-dom matchers.
- TypeScript configuration for tests: Ensure
tsconfig.json includes test files if you want type checking in tests, OR create a tsconfig.test.json that extends the base config with "types": ["vitest/globals", "@testing-library/jest-dom"] to avoid polluting production types.
- Install MSW for network mocking:
msw v2 with @mswjs/data is the production standard for intercepting fetch and Axios calls in tests without modifying application code. Initialize handlers in src/mocks/handlers.ts and the server in src/mocks/server.ts.
- Add jest-axe for accessibility testing:
jest-axe wraps the axe-core engine and integrates with jest/vitest matchers via expect(results).toHaveNoViolations(). This catches WCAG 2.1 AA violations automatically.
A minimal vitest.config.ts:
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./src/setupTests.ts'],
globals: true,
coverage: {
provider: 'v8',
thresholds: {
lines: 80,
branches: 75,
functions: 80,
statements: 80,
},
},
},
});
A minimal src/setupTests.ts:
import '@testing-library/jest-dom';
import { afterAll, afterEach, beforeAll } from 'vitest';
import { server } from './mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
2. Classify What You Are Testing and Apply the Right Pattern
React tests fall into four distinct categories. Identify the category before choosing the pattern:
- Unit: Pure UI components -- Components with no side effects, no data fetching, no context dependencies. Test rendering output for each significant prop combination. Use
screen.getByRole queries (not getByTestId) as the primary assertion mechanism. Verify accessible names, not implementation details like class names.
- Integration: Components with dependencies -- Components that consume context, call hooks that fetch data, or coordinate child components. Wrap in real or mocked providers. Use MSW to intercept API calls rather than mocking
fetch directly.
- Hook isolation tests -- Custom hooks tested with
renderHook from @testing-library/react. This is preferable to testing hooks only through the components that use them, which creates brittle coupling.
- Accessibility tests -- Run
axe on rendered output after all async operations have settled. These complement but do not replace manual screen reader testing.
Decision rule: if a test imports a component file and also imports a mock of a module the component depends on via vi.mock() or jest.mock(), it is an integration test regardless of size. Name it accordingly and expect it to be slower (200-800ms range is acceptable; over 2 seconds indicates a problem).
3. Write Queries Using the RTL Priority Hierarchy
The order in which you query the DOM determines how resilient tests are to UI refactoring. Follow this strict priority:
getByRole with { name: 'Submit' } -- matches by ARIA role and accessible name. This is the single most important query. Use it for buttons, inputs, headings, checkboxes, links, dialogs, and listbox items.
getByLabelText -- for form inputs associated with a <label>. Catches missing label associations, which are accessibility bugs.
getByPlaceholderText -- fallback for inputs without labels; use it only when you control the placeholder and label is genuinely absent by design.
getByText -- for non-interactive content. Use { exact: false } when text contains dynamic values.
getByDisplayValue -- for reading the current value of an input, select, or textarea.
getByAltText -- for images. Ensures alt attributes exist.
getByTitle -- rarely appropriate; tooltips and title attributes are accessibility anti-patterns in most cases.
getByTestId -- LAST RESORT. Reserve for cases where the element has no semantic meaning (e.g., a loading skeleton div). When you use data-testid, prefix it with the component name (data-testid="UserCard-avatar") to create a namespace.
Never query by CSS class name (.querySelector('.btn-primary')). Class names are implementation details and change during refactors without breaking functionality.
4. Type Mocks and Render Helpers Correctly in TypeScript
Untyped mocks are a source of silent errors. Apply these patterns:
- Typing vi.fn() / jest.fn() spy arguments: Use
vi.fn<Parameters<typeof originalFn>, ReturnType<typeof originalFn>>() or simply let TypeScript infer from the mock implementation. Do NOT cast with as jest.Mock -- this erases type safety.
- Partial mocks with vi.spyOn:
vi.spyOn(module, 'functionName').mockReturnValue(expectedValue) -- TypeScript will enforce that expectedValue matches the function's real return type.
- Typing mocked modules: When using
vi.mock('../api/users'), create a typed helper:
import { getUserById } from '../api/users';
vi.mock('../api/users');
const mockGetUserById = vi.mocked(getUserById);
vi.mocked() preserves the original function signature while allowing mock method calls like .mockResolvedValue().
- Custom render functions: Create a
src/test-utils/render.tsx that wraps RTL's render with all providers the app uses (QueryClientProvider, Router, ThemeProvider, Redux store). Type the wrapper's options parameter to extend RTL's RenderOptions:
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
initialRoute?: string;
}
function customRender(ui: ReactElement, : = {}) {
{ initialRoute = , ...renderOptions } = options;
(ui, {
: (
),
...renderOptions,
});
}
* ;
{ customRender render };
Import from everywhere instead of directly from .
5. Handle Async Behavior and State Updates
Async behavior is the most common source of flaky tests. Apply these patterns rigorously:
6. Mock External Dependencies Correctly
The correct mocking strategy depends on the dependency type:
- HTTP requests -- always use MSW: Define handlers in
src/mocks/handlers.ts using http.get('/api/users', () => HttpResponse.json([...])). Override specific handlers per test using server.use(http.get('/api/users', () => HttpResponse.json([], { status: 500 }))) to simulate error states. MSW intercepts at the network level, which means your actual fetch/axios configuration is exercised, not bypassed.
- Browser APIs not in jsdom: Mock
window.matchMedia, IntersectionObserver, ResizeObserver, and navigator.clipboard in setupTests.ts. Provide implementations that return sensible defaults.
- Timers: Use
vi.useFakeTimers() in beforeEach and vi.useRealTimers() in afterEach for tests involving setTimeout, setInterval, Date.now(), or debounce logic.
- Modules that import environment variables: Mock at the module level using
vi.mock('../config', () => ({ API_URL: 'http://localhost:3000' })). Never read process.env directly in component code -- centralize env access in a config module so it can be mocked cleanly.
- React Router navigation: Use
MemoryRouter with initialEntries prop for route-based tests. For useNavigate, spy on the navigate function using vi.spyOn rather than mocking the entire module.
- Do NOT mock child components unless they are third-party with heavy setup (e.g., a map library that requires canvas). Mocking child components makes tests brittle and hides integration bugs. Test the full component tree.
7. Test Custom Hooks with renderHook
Custom hooks deserve isolated tests separate from the components that consume them:
- Import
renderHook and act from @testing-library/react. Wrap state-updating calls in act() when calling them outside of RTL's async utilities.
- Wrap hooks that require providers using the
wrapper option:
const { result } = renderHook(() => useUserProfile(userId), {
wrapper: ({ children }) => <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>,
});
- Access the hook's return value via
result.current. After triggering updates, always re-read result.current -- the reference is live.
- For hooks that return async state, use
waitFor to poll until the expected state is reached:
await waitFor(() => expect(result.current.status).toBe('success'));
expect(result.current.data).toEqual(expectedUser);
- Test hook error states by configuring MSW to return error responses. Do not mock the hook's internal fetch calls directly -- that defeats the purpose of testing the hook's error handling logic.
8. Enforce Accessibility and Coverage Standards
Accessibility testing should be automated and mandatory, not optional:
Output Format
When helping a user implement or review React testing patterns, provide output in this structure:
## Test Strategy for [Component or Hook Name]
### Classification
- Test Category: [Unit | Integration | Hook | Accessibility]
- Dependencies: [List context providers, APIs, router, stores]
- Async Behavior: [Yes/No -- describe what is async]
### Setup
```typescript
// Required imports
import { render, screen, waitFor } from '../test-utils/render';
import userEvent from '@testing-library/user-event';
import { server } from '../mocks/server';
import { http, HttpResponse } from 'msw';
// MSW handler overrides for this test file
Test Cases
| Scenario | Query Strategy | Async | Assertion Type |
|---|
| Default render | getByRole | No | toBeInTheDocument |
| Loading state | getByRole('status') | Yes (findBy) | toBeInTheDocument |
| Success state | findByRole('list') | Yes (waitFor) | toHaveLength(n) |
| Error state | findByRole('alert') | Yes (waitFor) | toHaveTextContent |
| User interaction | getByRole('button') | Yes (await user.click) | onChange called |
| Accessibility | container | Yes (axe) | toHaveNoViolations |
Implementation
describe('[ComponentName]', () => {
const user = userEvent.setup();
describe('rendering', () => {
it('renders [scenario] correctly', async () => {
});
});
describe('interactions', () => {
it('calls [callback] when [action] occurs', async () => {
});
});
describe('async behavior', () => {
it('shows loading state then data', async () => {
});
});
describe('accessibility', () => {
it('has no WCAG violations', async () => {
});
});
});
Mocking Requirements
- MSW Handlers: [list endpoints and response shapes]
- Module Mocks: [list vi.mock calls with rationale]
- Browser API Stubs: [list any window/navigator mocks needed]
Coverage Targets
- Lines: [target %]
- Branches: [target %]
- Key paths to cover: [list critical conditional branches]
---
## Rules
1. **NEVER use `getByTestId` as the primary query strategy.** Every `data-testid` in production component code is a missed opportunity to enforce accessibility semantics. Reserve `getByTestId` for skeleton loaders, animation containers, and other elements with no semantic role. If you add more than 2 `data-testid` attributes in a single component, the component lacks proper ARIA structure -- fix the component.
2. **NEVER import directly from `@testing-library/react` in test files.** Always import from the custom `test-utils/render` wrapper that includes all providers. Tests that forget to wrap in providers produce false positives (the component renders with undefined context and may not crash, but the test is not realistic).
3. **NEVER use `fireEvent` when `userEvent` is available.** `fireEvent.click()` dispatches a single click DOM event. `userEvent.click()` dispatches the full sequence: `pointerover`, `pointerenter`, `mouseover`, `mouseenter`, `pointermove`, `mousemove`, `pointerdown`, `mousedown`, `pointerup`, `mouseup`, `click`. Components with hover states, focus traps, or drag interactions will behave differently under `fireEvent` than in a real browser.
4. **NEVER use `waitFor` with multiple assertions.** A single `waitFor` callback should assert exactly one condition. Multiple assertions inside `waitFor` create timing issues where the first assertion passes but the second fails intermittently. Chain `waitFor` calls or use `findBy` for the first async element, then use synchronous `expect` for subsequent assertions on elements already in the DOM.
5. **NEVER mock modules that are under test.** If you are testing `useAuthFlow`, do not mock `useAuthFlow`'s internal calls to `useTokenRefresh` unless `useTokenRefresh` makes real network calls (in which case, use MSW to mock the network, not the hook). Mocking internals means you are testing the mock, not the code.
6. **ALWAYS reset MSW handlers in `afterEach`.** Call `server.resetHandlers()` after each test. Failing to do so causes handler overrides from one test to bleed into subsequent tests, causing intermittent failures that are extremely difficult to diagnose.
7. **NEVER use snapshot tests for components with dynamic content** (dates, IDs, random values, user-generated content). Snapshots will fail on every run. Use inline assertions instead. Reserve snapshots for purely static components (icon sets, static marketing blocks) and commit to updating them deliberately.
8. **ALWAYS test the error boundary and Suspense fallback states explicitly.** The most common untested code path in React applications is the error state. Write at least one test per data-fetching component that configures MSW to return a 500 error, then asserts the error UI renders correctly and the error message is accessible.
9. **NEVER place `vi.mock()` or `jest.mock()` calls inside `describe` or `it` blocks.** Mock calls are hoisted to the top of the file by the bundler at compile time regardless of where you write them. Placing them inside blocks creates code that reads as if scoping applies when it does not, producing confusing behavior.
10. **ALWAYS clean up side effects in `afterEach`.** Call `vi.clearAllMocks()` (resets call history but keeps implementations) or `vi.resetAllMocks()` (resets implementations too) depending on whether mock implementations are set in `beforeEach`. Use `vi.restoreAllMocks()` only when you used `vi.spyOn` and want to restore the original implementation. Inconsistent cleanup is the root cause of over 60% of intermittent test failures in React projects.
---
## Edge Cases
### Testing Components Wrapped in Multiple Nested Providers
When a component requires 4+ context providers (Router + QueryClient + Redux + Theme + I18n), the `AllProviders` wrapper in `test-utils/render.tsx` can become unwieldy and tests become slow due to provider initialization. Create a `createTestQueryClient()` factory that returns a `QueryClient` with `retry: false` and `gcTime: 0` -- this prevents React Query from retrying failed requests (which would cause tests to hang for several seconds) and from caching data between tests. Set `staleTime: 0` so that components always refetch when they mount in tests, giving you control through MSW handlers. Create the `QueryClient` fresh in each test's `beforeEach`, not once at module level, to prevent query cache pollution between tests.
### Testing Components That Use Portals
Components that render into `document.body` via `ReactDOM.createPortal` (modals, tooltips, toasts) appear outside the container returned by `render()`. Query them using `screen.*` queries (which search the entire document) rather than `within(container).*` queries. When testing modal focus management, use `document.activeElement` to assert focus has moved into the portal. To test that `Escape` closes the modal, use `await user.keyboard('{Escape}')` -- not `fireEvent.keyDown` -- to correctly simulate the full keyboard event sequence.
### Testing Components with IntersectionObserver (Infinite Scroll, Lazy Images)
jsdom does not implement `IntersectionObserver`. Create a mock in `setupTests.ts`:
```typescript
const mockIntersectionObserver = vi.fn().mockImplementation((callback) => ({
observe: vi.fn().mockImplementation(() => callback([{ isIntersecting: true }])),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
window.IntersectionObserver = mockIntersectionObserver;
This mock fires the callback immediately with isIntersecting: true, simulating an element entering the viewport. For tests that need to verify behavior when NOT in the viewport, create an alternate mock that passes isIntersecting: false.
Testing Error Boundaries
React error boundaries require a component to throw during rendering. In tests, create a ThrowOnRender test component:
const ThrowOnRender = ({ shouldThrow }: { shouldThrow: boolean }) => {
if (shouldThrow) throw new Error('Test render error');
return <div>Safe</div>;
};
Suppress the expected console.error output that React prints when an error boundary catches an error -- otherwise it pollutes test output and can cause false CI failures. Use vi.spyOn(console, 'error').mockImplementation(() => {}) in beforeEach and restore it in afterEach. Assert that the error boundary's fallback UI renders and is accessible using axe.
Testing Components with useEffect Data Fetching Without React Query
Components that fetch in useEffect directly (using fetch or axios) require careful async handling because the fetch fires after render. Configure the MSW handler before rendering. Use await screen.findByRole(...) to wait for the loaded state rather than waitFor(() => screen.getByRole(...)) -- findBy has cleaner semantics for single-element async appearance. Always test the loading state by making the MSW handler delay its response: http.get('/api/data', async () => { await delay(100); return HttpResponse.json(data); }). Use vi.useFakeTimers() to control that delay precisely.
Testing Zustand or Jotai Stores in Isolation
Global stores that persist state across tests cause the most insidious test pollution. For Zustand, create a store with createStore (not create) so you can instantiate a fresh store per test:
const createTestStore = () => createStore<UserStore>()(userSlice);
let testStore: ReturnType<typeof createTestStore>;
beforeEach(() => { testStore = createTestStore(); });
Pass the test store into the component wrapper using the store's context provider. For Jotai, wrap each test's render in a <Provider store={createStore()}> where the store is created fresh. Never export a global singleton store from your store module -- always export the creator function.
Testing React 18 Concurrent Features (Transitions and Deferred Values)
useTransition and useDeferredValue use React's scheduler and do not flush synchronously in tests. When testing components that use startTransition, wrap the interaction that triggers the transition in act() explicitly, then use waitFor to assert the post-transition state. Be aware that isPending state from useTransition may be true for only a single render frame -- test it by intercepting a slow MSW response and asserting the pending indicator appears before the response arrives. Set the MSW handler delay to 50ms and assert isPending state within a waitFor with a timeout: 100.
Testing Components in a Monorepo with Shared Packages
When a tested component imports from a shared internal package (@company/design-system, @company/auth), configure the test runner's module resolver to use the TypeScript source of those packages rather than their compiled output. Add path aliases in vitest.config.ts under resolve.alias:
resolve: {
alias: {
'@company/design-system': path.resolve(__dirname, '../packages/design-system/src'),
},
}
This eliminates the need to rebuild shared packages before running tests, cutting CI time by 30-60% in monorepo setups. If shared packages have their own setupTests.ts, import them in the consuming package's setup file to avoid duplicating browser API mocks.
Example
Input: "I have a UserProfileCard component in TypeScript that fetches user data with React Query's useQuery, shows a loading skeleton, shows an error message on failure, and has a 'Follow' button that calls a POST endpoint. How do I test it properly?"
Output:
Test Strategy for UserProfileCard
Classification
- Test Category: Integration (React Query + network calls + user interaction)
- Dependencies: QueryClientProvider, React Query's
useQuery and useMutation, MSW for network interception
- Async Behavior: Yes -- initial data fetch on mount, mutation on button click
Setup
First, ensure MSW handlers cover both endpoints. In src/mocks/handlers.ts:
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users/:userId', ({ params }) => {
return HttpResponse.json({
id: params.userId,
name: 'Ada Lovelace',
bio: 'Mathematician and first programmer',
avatarUrl: 'https://example.com/ada.jpg',
followerCount: 1842,
isFollowedByCurrentUser: false,
});
}),
http.post('/api/users/:userId/follow', () => {
return HttpResponse.json({ success: true });
}),
];
In src/test-utils/render.tsx:
import { render, RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactElement } from 'react';
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: 0,
gcTime: 0,
},
mutations: {
retry: false,
},
},
});
}
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {}
export function render(ui: ReactElement, options: CustomRenderOptions = {}) {
const queryClient = createTestQueryClient();
return {
...(ui, {
: (
),
...options,
}),
queryClient,
};
}
* ;
Test File
import { render, screen, waitFor, within } from '../test-utils/render';
import userEvent from '@testing-library/user-event';
import { server } from '../mocks/server';
import { http, HttpResponse, delay } from 'msw';
import { axe } from 'jest-axe';
import { UserProfileCard } from './UserProfileCard';
describe('UserProfileCard', () => {
const user = userEvent.setup();
const testUserId = 'user-123';
describe('loading state', () => {
it('renders a loading skeleton while fetching user data', async () => {
server.use(
http.get('/api/users/:userId', async () => {
await delay(200);
return HttpResponse.({ : testUserId, : });
})
);
();
(screen.(, { : }))
.();
( {
(screen.(, { : }))
..();
});
});
});
(, {
(, () => {
();
heading = screen.(, { : });
(heading).();
(screen.()).();
(screen.()).();
});
(, () => {
();
followButton = screen.(, { : });
(followButton).();
(followButton).(, );
});
});
(, {
( {
vi.(, ).( {});
});
( {
vi.(.).();
});
(, () => {
server.(
http.(, {
.(
{ : },
{ : }
);
})
);
();
alert = screen.();
(alert).();
((alert).(, { : }))
.();
});
(, () => {
server.(
http.(, {
.();
})
);
();
screen.();
(screen.())..();
});
});
(, {
(, () => {
();
followButton = screen.(, { : });
user.(followButton);
( {
(
screen.(, { : })
).(, );
});
(screen.()).();
});
(, () => {
server.(
http.(, {
.({ : }, { : });
})
);
();
followButton = screen.(, { : });
user.(followButton);
screen.();
(screen.()).();
( {
(
screen.(, { : })
).(, );
});
(screen.()).();
});
(, () => {
server.(
http.(, () => {
();
.({ : });
})
);
();
followButton = screen.(, { : });
user.(followButton);
(followButton).();
( {
(
screen.(, { : })
)..();
});
});
});
(, {
(, () => {
{ container } = ();
screen.(, { : });
results = (container);
(results).();
});
(, () => {
server.(
http.(, {
.({}, { : });
})
);
{ container } = ();
screen.();
results = (container);
(results).();
});
});
});
Test Cases Summary
| Scenario | Query Strategy | Async | Primary Assertion |
|---|
| Loading skeleton visible | getByRole('status') | No (synchronous) | toBeInTheDocument |
| Loading skeleton disappears | queryByRole('status') | Yes (waitFor) | not.toBeInTheDocument |
| User name rendered | findByRole('heading') | Yes (findBy) | toBeInTheDocument |
| Follower count formatted | getByText('1,842 followers') | No (after await) | toBeInTheDocument |
| Follow button unfollowed state | findByRole('button', { name }) | Yes (findBy) | toHaveAttribute('aria-pressed', 'false') |
| 500 error shows alert | findByRole('alert') | Yes (findBy) | toHaveTextContent |
| Network error shows alert | findByRole('alert') | Yes (findBy) | Not to stringify error object |
| Follow success updates UI | getByRole('button', { name }) | Yes (waitFor) | aria-pressed: true, count incremented |
| Follow failure reverts UI | getByRole('alert') | Yes (findBy) | Count unchanged, aria-pressed: false |
| Mutation in flight disables button | followButton (retained ref) | Yes (waitFor) | toBeDisabled then not.toBeDisabled |
| Loaded state axe scan | container | Yes (findByRole first) | toHaveNoViolations |
Mocking Requirements
- MSW Handlers:
GET /api/users/:userId returns user object; POST /api/users/:userId/follow returns { success: true }
- Per-test overrides: 500 error on GET, network failure on GET, 403 on POST follow, delayed POST follow
- Module Mocks: None -- React Query, fetch, and component internals are all tested through real implementations intercepted by MSW
- Browser API Stubs: None required for this component unless it uses IntersectionObserver for avatar lazy loading
Coverage Targets
- Lines: 90%+ (this is a critical user-facing component)
- Branches: 85%+ (all conditional render paths: loading, error, success, follow/following state, mutation pending state)
- Key branches to cover:
isLoading, isError, isSuccess, mutation.isPending, isFollowedByCurrentUser, follower count formatting edge cases (0 followers, 1 follower, 1000+)