claude/skills/react-testing/SKILL.md
React/TypeScriptコンポーネントのテスト設計・実装ガイド。FPアプローチでIOを分離し、vi.mockを使わずに振る舞いテストを書く。「テストを設計したい」「コンポーネントテストを書きたい」「テスタビリティを改善したい」「vi.mockを使わずにテストしたい」などで起動。対象コンポーネントの分析→実装改善→テスト設計→テスト実装の4ステップで実行する。
npx skillsauth add skanehira/dotfiles react-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
React/TypeScriptコンポーネントの振る舞いテストを、FPアプローチで設計・実装する。
対象ディレクトリの全ファイルを読み、各コンポーネントを分類:
| 分類 | 特徴 | テスト | | ------------ | ----------------------------- | --------------------------- | | 純粋表示 | props→JSXのみ、状態・分岐なし | スキップ | | ロジック内包 | フィルタ、URL構築等の計算 | 純粋関数抽出→テスト | | IO依存 | useSWR、API呼び出し | DI化→振る舞いテスト | | 状態管理 | Context、useState | Provider注入→振る舞いテスト |
テスト不要(primitive)の例:
({ open }) => <svg className={open ? "rotate-90" : ""} />({ children }) => <div className="container">{children}</div>export const LABEL = "送信"テスト必要(振る舞いあり)の例:
コンポーネント内のロジックを同ディレクトリの utils.ts に抽出:
// components/example/utils.ts
export function filterItems(items: Item[], query: string): Item[] {
if (!query) return items;
return items.filter(item => item.name.includes(query));
}
export const yearKey = (year: number) => `y:${year}`;
export function buildItemLink(id: number, params: Record<string, string>): string {
const sp = new URLSearchParams(params);
return `/items/${id}?${sp.toString()}`;
}
抽出基準: 入力→出力の変換で、DOM・API・ブラウザAPIに触れない処理。
API呼び出し関数を直接importせず、ContextでDI:
// Before: APIを直接import(テスト時vi.mock必要)
import { fetchData } from "../../lib/api";
const { data } = useSWR(key, () => fetchData(params));
// After: Contextから注入(テスト時フェイク注入)
const { fetchData } = useMyContext();
const { data } = useSWR(key, () => fetchData(params));
検証: 改善後に npm run build でビルド通過を確認。
2層に分ける:
層1: 純粋関数テスト (utils.test.ts) — Reactなし、入力→出力の検証:
import { filterItems, yearKey } from "./utils";
describe("filterItems", () => {
it("returns all items when query is empty", () => {
const items = [{ name: "foo" }, { name: "bar" }];
expect(filterItems(items, "")).toEqual(items);
});
it("filters items by name", () => {
const items = [{ name: "foo" }, { name: "bar" }];
expect(filterItems(items, "foo")).toEqual([{ name: "foo" }]);
});
});
層2: コンポーネント振る舞いテスト (*.test.tsx) — ユーザー操作→画面変化:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router";
import { SWRConfig } from "swr";
function renderWith(ui: React.ReactElement) {
return render(
<SWRConfig value={{ provider: () => new Map() }}>
<MemoryRouter>
<MyProvider value={{ fetchData: fakeFetch }}>
{ui}
</MyProvider>
</MemoryRouter>
</SWRConfig>
);
}
it("shows filtered results when user types", async () => {
renderWith(<MyCombobox items={testItems} onSelect={vi.fn()} />);
await userEvent.type(screen.getByPlaceholder("検索"), "foo");
expect(screen.getByText("foo")).toBeVisible();
expect(screen.queryByText("bar")).not.toBeInTheDocument();
});
ファイル配置はコロケーション:
components/example/
├── utils.ts ← 純粋関数
├── utils.test.ts ← 純粋関数テスト
├── MyComponent.tsx
├── MyComponent.test.tsx ← 振る舞いテスト
└── index.ts
実装上の注意:
findByText(自動リトライ)で待つ。getByText は即座に失敗するact ワーニングが出たら await findByText や waitFor で状態更新の完了を待つSWRConfig の provider: () => new Map() でテスト間のキャッシュ汚染を防ぐテスト実装・修正後に、プロジェクトのフォーマッター・リンターを実行する。
コマンドの特定方法(以下の順で確認):
CLAUDE.md に記載されたコマンド(最優先)justfile / Makefile のタスク(例: just fmt, just lint)package.json の scripts(例: npm run lint, npm run format)vp check --fix, deno fmt, cargo fmt)実行タイミング: テストが全件通過した後、コミット前に必ず実行する。
パッケージ: vitest, @testing-library/react, @testing-library/jest-dom, @testing-library/user-event, jsdom
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: { environment: "jsdom", setupFiles: ["./src/test-setup.ts"] },
});
// src/test-setup.ts
import "@testing-library/jest-dom/vitest";
| やらない | 代わりに |
| ----------------------------- | ---------------------------- |
| vi.mock("swr") | SWRConfig + DI |
| vi.mock("../lib/api") | Context経由でフェイク注入 |
| wrapper.find(".class-name") | screen.getByRole("button") |
| expect(state).toBe(...) | 操作後の画面変化を検証 |
| 全コンポーネントにテスト | primitiveはスキップ |
| data-testid 乱用 | アクセシブルなロケータ優先 |
tools
ローカルのコミット履歴と差分からDraft PRを作成する。ブランチ未作成・コミット未作成の状態でも、必要に応じてブランチ作成とコミットを行ってからPRを作成する。`.github/` にPRテンプレートがあれば内容を埋めて、なければ作業内容から本文を生成し、`AskUserQuestion`で作成可否を確認してから `gh pr create --draft` を実行する。「PRを出したい」「draft PRを作成」「プルリクを作って」「PR本文を生成」などのリクエストで起動。
tools
複数サブエージェントに異なる立場を与えて議論を反復し、相違が収束するまで議題を検証して結論を提示する。設計妥当性検証・実装方針比較・原因分析のセカンドオピニオン・アイデアの壁打ちに使用。「議論したい」「壁打ちしたい」「セカンドオピニオン」「複数視点で検証したい」などで起動。
tools
変更内容を分析し、Conventional Commit形式でコミットする (pushはユーザが手動)
development
React 19 + Vite+ (`vp`) + TypeScript + Tailwind CSS v4 + React Router v7 (HashRouter) でモバイル向け静的SPAデモサイトをTDDで構築し、Cloudflare Workers (Static Assets) へ自動デプロイするまでの標準ワークフローを提供する。テンプレートリポジトリ `skanehira/demo-site-template` を `gh repo create --template` で clone することで scaffold を省略する。`localStorage` でフロントエンドのみ完結する"フロントのみ完結デモ"に特化。デザインコンセプトの確立には `frontend-design` スキルを呼び出して連携する。起動トリガー:「デモサイトを作りたい」「モバイル向け静的デモ」「SPAを作ってCloudflareにデプロイ」「静的プロトタイプを公開」「localStorage でフロントだけ完結」。ユースケース:(1)クライアント提案用のUI/UXたたき台、(2)新機能のプロトタイプ、(3)モバイル向けランディング。ツールチェーンは Vite+ (`vp`) で統合(内部 PM は pnpm)。