component-testing

jpmorgan-payments's avatarfrom jpmorgan-payments

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.

⭐18stars🔀7forks📁View on GitHub🕐Updated Jan 3, 2026

When & Why to Use This Skill

This Claude skill provides a comprehensive framework for React component testing using Vitest, MSW (Mock Service Worker), and TanStack Query. It establishes standardized patterns for API mocking, user interaction simulation, and context provider testing, enabling developers to achieve high-quality code with a minimum of 80% test coverage through consistent and maintainable testing practices.

Use Cases

  • Mocking REST API interactions with MSW 2.x to simulate various server responses, including success, 400 validation errors, and 500 server failures.
  • Testing complex React components that rely on React Query for data fetching, ensuring proper handling of loading skeletons and error alerts.
  • Simulating realistic user interactions using React Testing Library and user-event to verify UI behavior and state changes.
  • Setting up standardized test environments by wrapping components in necessary providers like QueryClientProvider and custom UI providers.
  • Ensuring high code quality and coverage by implementing tests for edge cases, accessibility features, and form validation logic.
namecomponent-testing
descriptionComprehensive 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.
compatibilityDesigned for Vitest with React Testing Library, MSW 2.x, React Query v5
version"2.0.0"
authorjpmorgan-payments
lastUpdated"2025-12-24"
prioritycritical

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

  1. Test component rendering with different prop combinations
  2. Verify user interactions and their effects
  3. Test error states and loading states
  4. Ensure proper API interaction handling
  5. Test accessibility features (ARIA, keyboard nav)
  6. 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.md for architecture patterns
  • See .github/copilot/prompts/run-tests-and-fix.md for testing workflows