From zenbu-powers
React Integration Test 程式碼品質規範合集。包含 SOLID 設計原則、 Component/Hook 組織規範、Testing-Library 最佳實踐、TypeScript 嚴格型別、 MSW Handler 品質。供 refactor 階段嚴格遵守。
npx claudepluginhub zenbuapps/zenbu-powers --plugin zenbu-powersThis skill uses the workspace's default tool permissions.
供重構階段嚴格遵守。涵蓋 SOLID、測試組織、Meta 清理、程式架構、程式碼品質。
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Guides code writing, review, and refactoring with Karpathy-inspired rules to avoid overcomplication, ensure simplicity, surgical changes, and verifiable success criteria.
Share bugs, ideas, or general feedback.
供重構階段嚴格遵守。涵蓋 SOLID、測試組織、Meta 清理、程式架構、程式碼品質。
❌ Bad — Component 同時做資料擷取、狀態管理、渲染:
function LessonProgressPage({ userId }: { userId: string }) {
const [data, setData] = useState<LessonProgress[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}/progress`)
.then((res) => res.json())
.then((json) => setData(json))
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <ul>{data.map((d) => <li key={d.id}>{d.lessonId}</li>)}</ul>;
}
✅ Good — Component 僅渲染,資料擷取交給 custom hook:
function LessonProgressPage({ userId }: { userId: string }) {
const { data, isLoading, error } = useLessonProgress(userId);
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error.message} />;
return <LessonProgressList items={data ?? []} />;
}
// 可透過 children 擴展而不修改 Card
function Card({ title, children }: { title: string; children: ReactNode }) {
return (
<section>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
}
ButtonPrimary 與 ButtonSecondary 皆宣告繼承 ButtonProps,則兩者必須可在同樣的上下文中互換❌ Bad:
function UserAvatar({ user }: { user: User }) {
return <img src={user.avatarUrl} alt={user.displayName} />;
}
✅ Good:
interface UserAvatarProps {
avatarUrl: string;
displayName: string;
}
function UserAvatar({ avatarUrl, displayName }: UserAvatarProps) {
return <img src={avatarUrl} alt={displayName} />;
}
// hooks/useLessonProgress.ts
export function useLessonProgress(userId: string) {
return useQuery({
queryKey: ['lessonProgress', userId],
queryFn: () => apiClient.getLessonProgress(userId),
});
}
// components/LessonProgressPage.tsx — 不直接觸碰 fetch/axios
function LessonProgressPage({ userId }: { userId: string }) {
const { data } = useLessonProgress(userId);
return <LessonProgressList items={data ?? []} />;
}
依序使用:
getByRolegetByLabelTextgetByPlaceholderTextgetByTextgetByDisplayValuegetByAltTextgetByTitlegetByTestId(最後手段)userEvent(而非 fireEvent)——更貼近真實使用者行為userEvent.setup() 以保證事件順序正確await user.click(button),而非 fireEvent.click(button)import userEvent from '@testing-library/user-event';
it('submits form', async () => {
const user = userEvent.setup();
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'user@example.com');
await user.type(screen.getByLabelText(/password/i), 'secret');
await user.click(screen.getByRole('button', { name: /log in/i }));
expect(await screen.findByText(/welcome/i)).toBeInTheDocument();
});
waitFor 處理依賴非同步操作的斷言findBy* 處理非同步出現的元素setTimeout 或固定延遲waitFor 預設逾時為 1000ms,僅在有正當理由時才延長// ✅ 等待非同步出現的元素
expect(await screen.findByText(/success/i)).toBeInTheDocument();
// ✅ 自訂條件
await waitFor(() => {
expect(screen.getByRole('status')).toHaveTextContent(/complete/i);
});
screen.getByRole() 等(不要解構 render 回傳值)within() 將查詢限縮於特定容器const row = screen.getByRole('row', { name: /lesson 1/i });
expect(within(row).getByText(/completed/i)).toBeInTheDocument();
any——必要時使用 unknown + type guardtype Foo = z.infer<typeof FooSchema>interface 定義(而非 type)// ✅ Zod schema 推導
const LessonProgressSchema = z.object({
id: z.string(),
userId: z.string(),
lessonId: z.number(),
progress: z.number(),
status: z.enum(['NOT_STARTED', 'IN_PROGRESS', 'COMPLETED']),
});
export type LessonProgress = z.infer<typeof LessonProgressSchema>;
// ✅ Hook 明確回傳型別
export function useLessonProgress(
userId: string,
): UseQueryResult<LessonProgress[], Error> {
return useQuery({ ... });
}
// ✅ Props 使用 interface
interface LessonCardProps {
lessonId: number;
title: string;
progress: number;
}
.feature 對應一個測試檔{feature}.integration.test.tsxdescribe() block 對應 Feature / Rule 結構it() block 對應 ExamplesbeforeEach,不在各個 it() 重複src/__tests__/helpers/src/__tests__/factories/src/__tests__/mocks/@testing-library/*、vitest、msw)// 1. framework
import React from 'react';
// 2. testing libraries
import { describe, it, expect, beforeEach } from 'vitest';
import { screen, within } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
// 3. component under test
import { LessonProgressPage } from '@/pages/LessonProgressPage';
// 4. test helpers
import { renderWithProviders } from '@/test/helpers/render';
import { createUser } from '@/test/helpers/user-event';
import { mockLessonProgress } from '@/test/factories/lesson-progress.factory';
import { server } from '@/test/mocks/server';
// 5. types
import type { LessonProgress } from '@/lib/types/schemas';
handlers.tsserver.use() 處理,並於 afterEach 重置http.get()、http.post() 等)import { http, HttpResponse } from 'msw';
import type { LessonProgress } from '@/lib/types/schemas';
export const handlers = [
http.get('/api/users/:userId/progress', () => {
const body: LessonProgress[] = [];
return HttpResponse.json(body);
}),
];
pages/ (routing + layout)
└── components/ (UI rendering)
└── hooks/ (data fetching + state)
└── lib/api/ (API client functions)
LessonProgressCard)use 為前綴(useLessonProgress)handle 為前綴(handleSubmit、handleProgressUpdate)is / has / can 為前綴(isLoading、hasError)function LessonProgressPage({ userId }: { userId: string }) {
const { data, isLoading, error } = useLessonProgress(userId);
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error.message} />;
if (!data || data.length === 0) return <EmptyState />;
return <LessonProgressList items={data} />;
}
// TODO: [States Prepare: ...]// TODO: [Operation Invocation: ...]// TODO: [Result Verifier: ...]// TODO: [States Verify: ...]getByRole 優先(Testing-Library best practice)userEvent,非 fireEventwaitFor / findBy* 處理非同步any 型別afterEach 重置 MSW handlers