원클릭으로
testing-helper
Testing patterns for React and React Router v7 - Vitest, React Testing Library, route testing, mocking loaders/actions
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Testing patterns for React and React Router v7 - Vitest, React Testing Library, route testing, mocking loaders/actions
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Comprehensive React component testing with Jest and React Testing Library covering configuration, mocking strategies, async testing patterns, hooks testing, and integration testing best practices
TypeScript patterns for React Router v7 - Type-safe loaders, actions, params, generic components, and utility types
TypeScript library starter using tsdown. Use when scaffolding or maintaining a TS/ESM library with tsdown, pnpm, Vitest, and npm Trusted Publisher.
Navigate, manage, and optimize monorepos with Turborepo, Nx, pnpm workspaces, and Changesets. Covers cross-package impact analysis, selective builds, dependency graph visualization, remote caching, migration from multi-repo, and coordinated publishing. Use when working in monorepos, optimizing build times, or managing shared packages.
Next.js 15 App Router patterns - use for frontend pages, API routes, server components, client components, and middleware
Comprehensive guidance on setting up npm libraries with package.json, with a preference for ES Modules (ESM). Use when setting up npm packages, configuring ESM, TypeScript packages, or React component libraries.
| name | testing-helper |
| description | Testing patterns for React and React Router v7 - Vitest, React Testing Library, route testing, mocking loaders/actions |
| tags | ["testing","vitest","react-testing-library","react-router"] |
| version | 1.0.0 |
| author | Code Visionary |
Master testing for React and React Router v7 applications. Learn how to write effective tests using Vitest and React Testing Library.
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
test("renders button", () => {
render(<Button>Click me</Button>);
expect(screen.getByText("Click me")).toBeInTheDocument();
});
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("handles click", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click</Button>);
await user.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledOnce();
});
import { loader } from "./route";
test("loader fetches user", async () {
const result = await loader({
params: { userId: "123" },
request: new Request("http://localhost"),
context: {},
});
expect(result.user).toBeDefined();
});
npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
setupFiles: ["./test/setup.ts"],
globals: true,
},
});
// test/setup.ts
import "@testing-library/jest-dom";
import { expect, afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
// Cleanup after each test
afterEach(() => {
cleanup();
});
import { render, screen } from "@testing-library/react";
import { describe, test, expect } from "vitest";
import { UserCard } from "./UserCard";
describe("UserCard", () => {
test("renders user information", () => {
const user = {
id: "1",
name: "John Doe",
email: "john@example.com",
};
render(<UserCard user={user} />);
expect(screen.getByText("John Doe")).toBeInTheDocument();
expect(screen.getByText("john@example.com")).toBeInTheDocument();
});
test("displays avatar when provided", () => {
const user = {
id: "1",
name: "John Doe",
email: "john@example.com",
avatar: "https://example.com/avatar.jpg",
};
render(<UserCard user={user} />);
const avatar = screen.getByRole("img", { name: "John Doe" });
expect(avatar).toHaveAttribute("src", user.avatar);
});
});
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
test("calls onClick when button is clicked", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByRole("button", { name: "Click me" }));
expect(handleClick).toHaveBeenCalledOnce();
});
test("types in input field", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<input onChange={handleChange} />);
await user.type(screen.getByRole("textbox"), "Hello");
expect(screen.getByRole("textbox")).toHaveValue("Hello");
expect(handleChange).toHaveBeenCalledTimes(5); // Once per character
});
import { render, screen, waitFor } from "@testing-library/react";
test("loads and displays data", async () => {
render(<UserProfile userId="123" />);
// Initially shows loading
expect(screen.getByText("Loading...")).toBeInTheDocument();
// Wait for data to load
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
// Loading indicator is gone
expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
});
import { loader } from "./route";
import { vi } from "vitest";
// Mock fetch
global.fetch = vi.fn();
test("loader returns user data", async () => {
const mockUser = { id: "123", name: "John" };
(fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockUser,
});
const result = await loader({
params: { userId: "123" },
request: new Request("http://localhost/users/123"),
context: {},
});
expect(result).toEqual({ user: mockUser });
expect(fetch).toHaveBeenCalledWith("/api/users/123");
});
test("loader throws on 404", async () => {
(fetch as any).mockResolvedValueOnce({
ok: false,
status: 404,
});
await expect(loader({
params: { userId: "999" },
request: new Request("http://localhost/users/999"),
context: {},
})).rejects.toThrow();
});
import { action } from "./route";
test("action creates user on valid data", async () => {
const formData = new FormData();
formData.set("name", "John Doe");
formData.set("email", "john@example.com");
const request = new Request("http://localhost/users", {
method: "POST",
body: formData,
});
const result = await action({
request,
params: {},
context: {},
});
expect(result).toHaveProperty("user");
});
test("action returns errors on invalid data", async () => {
const formData = new FormData();
formData.set("name", "");
formData.set("email", "invalid");
const request = new Request("http://localhost/users", {
method: "POST",
body: formData,
});
const result = await action({
request,
params: {},
context: {},
});
expect(result).toHaveProperty("errors");
expect(result.errors).toHaveProperty("name");
expect(result.errors).toHaveProperty("email");
});
import { render, screen } from "@testing-library/react";
import { createMemoryRouter, RouterProvider } from "react-router";
test("renders route component", async () => {
const router = createMemoryRouter(
[
{
path: "/users/:userId",
element: <UserProfile />,
loader: async () => ({ user: { id: "1", name: "John" } }),
},
],
{
initialEntries: ["/users/1"],
}
);
render(<RouterProvider router={router} />);
await screen.findByText("John");
});
import { vi } from "vitest";
// Mock fetch globally
global.fetch = vi.fn();
// Mock specific responses
(fetch as any).mockResolvedValue({
ok: true,
json: async () => ({ data: "mocked" }),
});
// Clean up after test
afterEach(() => {
vi.clearAllMocks();
});
import { vi } from "vitest";
// Mock entire module
vi.mock("./api", () => ({
fetchUser: vi.fn(),
createUser: vi.fn(),
}));
// Import mocked module
import { fetchUser } from "./api";
test("uses mocked API", async () => {
(fetchUser as any).mockResolvedValue({ id: "1", name: "John" });
const user = await fetchUser("1");
expect(user).toEqual({ id: "1", name: "John" });
});
import { vi } from "vitest";
import * as ReactRouter from "react-router";
vi.spyOn(ReactRouter, "useNavigate").mockReturnValue(vi.fn());
vi.spyOn(ReactRouter, "useLoaderData").mockReturnValue({
user: { id: "1", name: "John" },
});
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createMemoryRouter, RouterProvider } from "react-router";
test("submits form with valid data", async () => {
const user = userEvent.setup();
const actionSpy = vi.fn().mockResolvedValue({ success: true });
const router = createMemoryRouter(
[
{
path: "/create",
element: <CreateUserForm />,
action: actionSpy,
},
],
{
initialEntries: ["/create"],
}
);
render(<RouterProvider router={router} />);
// Fill form
await user.type(screen.getByLabelText("Name"), "John Doe");
await user.type(screen.getByLabelText("Email"), "john@example.com");
// Submit
await user.click(screen.getByRole("button", { name: "Submit" }));
// Verify action was called
expect(actionSpy).toHaveBeenCalled();
});
test("displays validation errors", async () => {
const user = userEvent.setup();
const actionSpy = vi.fn().mockResolvedValue({
errors: { email: ["Invalid email"] },
});
const router = createMemoryRouter(
[
{
path: "/create",
element: <CreateUserForm />,
action: actionSpy,
},
],
{
initialEntries: ["/create"],
}
);
render(<RouterProvider router={router} />);
await user.type(screen.getByLabelText("Email"), "invalid");
await user.click(screen.getByRole("button", { name: "Submit" }));
await screen.findByText("Invalid email");
});
screen queries over destructured render resultgetByRole over other query methodsuserEvent instead of fireEventwaitFor for async assertionsUse queries in this order:
getByRole - Most accessiblegetByLabelText - Good for formsgetByPlaceholderText - Form fallbackgetByText - User-visible textgetByTestId - Last resort// ✅ Best - accessible
screen.getByRole("button", { name: "Submit" });
screen.getByRole("textbox", { name: "Email" });
// ✅ Good - form labels
screen.getByLabelText("Email");
// ⚠️ Okay - if no role/label
screen.getByPlaceholderText("Enter email");
// ⚠️ Fallback
screen.getByText("Submit");
// ❌ Last resort
screen.getByTestId("submit-button");
Symptoms: "Warning: An update to Component was not wrapped in act()"
Cause: State updates not properly awaited
Solution: Use waitFor or findBy queries
// ❌ Causes act warning
render(<AsyncComponent />);
expect(screen.getByText("Loaded")).toBeInTheDocument();
// ✅ Waits for update
render(<AsyncComponent />);
await screen.findByText("Loaded");
Symptoms: "Unable to find an element" Cause: Wrong query or timing issue Solution: Use correct query method and wait for element
// ❌ Element not visible yet
expect(screen.getByText("Success")).toBeInTheDocument();
// ✅ Wait for element to appear
await screen.findByText("Success");
// Or check if it doesn't exist
expect(screen.queryByText("Error")).not.toBeInTheDocument();