| name | component-testing |
| description | Comprehensive testing patterns for React components with MSW, React Query, and Vitest. Use when writing tests, setting up mocks, testing API interactions, or ensuring 80% coverage. Keywords - testing, MSW, React Query, Vitest, mocks, coverage, API testing. |
| compatibility | Designed for Vitest with React Testing Library, MSW 2.x, React Query v5 |
| metadata | [object Object] |
Component Testing Patterns
Overview
All components require comprehensive tests with minimum 80% coverage. Tests must be colocated with implementation files (not in separate __tests__/ directories).
Test File Requirements
File naming: ComponentName.test.tsx next to ComponentName.tsx
Basic structure:
import { render, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@test-utils";
import { http, HttpResponse } from "msw";
import { server } from "@/msw/server";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { EBComponentsProvider } from "@/providers/EBComponentsProvider";
// Setup QueryClient for tests
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
// Mock data setup
const mockData = {
// Component-specific mock data
};
Test Setup Helper Function
Always create a renderComponent helper:
const renderComponent = (props = {}) => {
// Reset MSW handlers before each render
server.resetHandlers();
// Setup explicit API mock handlers
server.use(
http.get("/api/endpoint", () => {
return HttpResponse.json(mockData);
})
);
return render(
<EBComponentsProvider
apiBaseUrl="/"
headers={{}}
contentTokens={{ name: "enUS" }}
>
<QueryClientProvider client={queryClient}>
<ComponentName {...props} />
</QueryClientProvider>
</EBComponentsProvider>
);
};
Test Structure
describe("ComponentName", () => {
// Mock external dependencies
vi.mock("@/components/ui/someComponent", () => ({
useSomeHook: () => ({ someFunction: vi.fn() }),
}));
test("renders correctly with initial data", async () => {
renderComponent();
// Wait for async operations
await waitFor(() => {
expect(screen.getByText(/expected text/i)).toBeInTheDocument();
});
});
test("handles user interactions", async () => {
renderComponent();
// Simulate user actions
await userEvent.click(screen.getByRole("button", { name: /action/i }));
// Verify state changes
expect(screen.getByText(/new state/i)).toBeInTheDocument();
});
test("handles API interactions", async () => {
// Setup specific API mock for this test
server.use(
http.post("/api/endpoint", () => {
return HttpResponse.json({ success: true });
})
);
renderComponent();
// Trigger API call
await userEvent.click(screen.getByRole("button", { name: /submit/i }));
// Verify API interaction results
await waitFor(() => {
expect(screen.getByText(/success/i)).toBeInTheDocument();
});
});
});
MSW API Mocking Patterns
// Setup mock handlers
server.use(
// GET requests
http.get("/api/resource/:id", ({ params }) => {
return HttpResponse.json(mockData[params.id]);
}),
// POST requests with response validation
http.post("/api/resource", async ({ request }) => {
const body = await request.json();
if (!isValidData(body)) {
return HttpResponse.json(
{ error: "Invalid data" },
{ status: 400 }
);
}
return HttpResponse.json({ success: true });
}),
// Error scenarios
http.get("/api/error-case", () => {
return HttpResponse.json(
{ error: "Something went wrong" },
{ status: 500 }
);
})
);
Context and Provider Testing
// For components using multiple contexts
const renderWithProviders = (ui: React.ReactElement) => {
return render(
<EBComponentsProvider apiBaseUrl="/" headers={{}}>
<FeatureContextProvider value={mockFeatureContext}>
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>
</FeatureContextProvider>
</EBComponentsProvider>
);
};
// Testing context updates
test("responds to context changes", async () => {
const { rerender } = renderWithProviders(<ComponentName />);
// Verify initial state
expect(screen.getByText(/initial/i)).toBeInTheDocument();
// Rerender with new context
rerender(
<FeatureContextProvider value={newMockContext}>
<ComponentName />
</FeatureContextProvider>
);
// Verify updated state
expect(screen.getByText(/updated/i)).toBeInTheDocument();
});
Testing Best Practices
- Test component rendering with different prop combinations
- Verify user interactions and their effects
- Test error states and loading states
- Ensure proper API interaction handling
- Test accessibility features (ARIA, keyboard nav)
- Cover edge cases and validation scenarios
Test Coverage Requirements
- ✅ Minimum 80% line coverage
- ✅ Cover all user interaction paths
- ✅ Test all API integration points
- ✅ Verify error handling and edge cases
- ✅ Test accessibility features
- ✅ Include integration tests for complex flows
Query Key Management
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { getSmbdoGetClientQueryKey } from "@/api/generated/smbdo";
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data) => api.post("/endpoint", data),
onSuccess: (response) => {
// Invalidate related queries
queryClient.invalidateQueries({
queryKey: getSmbdoGetClientQueryKey(clientId),
});
},
});
Common Patterns
Testing Forms
test("validates form inputs", async () => {
renderComponent();
const input = screen.getByLabelText(/email/i);
await userEvent.type(input, "invalid-email");
await userEvent.click(screen.getByRole("button", { name: /submit/i }));
expect(screen.getByText(/invalid email/i)).toBeInTheDocument();
});
Testing Loading States
Use skeleton components, never "Loading..." text:
test("shows loading state", async () => {
renderComponent();
// Check for skeleton loader
expect(screen.getByTestId("skeleton")).toBeInTheDocument();
// Wait for data to load
await waitFor(() => {
expect(screen.queryByTestId("skeleton")).not.toBeInTheDocument();
});
});
Testing Error States
Always use ServerErrorAlert component:
test("displays error alert", async () => {
server.use(
http.get("/api/endpoint", () => {
return HttpResponse.json(
{ error: "Server error" },
{ status: 500 }
);
})
);
renderComponent();
await waitFor(() => {
expect(screen.getByText(/server error/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /try again/i })).toBeInTheDocument();
});
});
Running Tests
After ANY code change, you MUST:
cd embedded-components
yarn test
This runs: typecheck → format:check → lint → test:unit
Quick fix commands:
cd embedded-components
yarn format # Auto-fix formatting
yarn lint:fix # Auto-fix linting
yarn typecheck # Check types only
yarn test:unit # Run tests only
Never Commit Code With
- ❌ TypeScript errors
- ❌ Formatting errors (Prettier)
- ❌ Linting errors
- ❌ Failing tests
References
- See
embedded-components/ARCHITECTURE.mdfor architecture patterns - See
.github/copilot/prompts/run-tests-and-fix.mdfor testing workflows