Files
writer-work-flow/apps/web/lib/review/useAccept.test.ts
Yaojia Wang a6f5d085e5 test: 接入 jsdom 测试栈, 把 15 个 React hooks 纳入 80% 覆盖率门禁
新增 jsdom + @testing-library/react, 为 lib/** 全部 use*.ts hooks 写 renderHook 单测:
生成/CRUD(world/character/generator/outline/accept/foreshadow/rules)、
SSE流(draftStream/reviewStream)、定时轮询(autosave/jobPoll/kimiOauth)、
文风与注入(refine/styleLearn/injection)。覆盖率 mock api 客户端+Toast, 不打真实 LLM。
vitest.config.ts 去掉 use*.ts 排除; 前端覆盖率 63%→95%。CLAUDE.md 同步说明。
2026-06-27 06:21:42 +02:00

133 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @vitest-environment jsdom
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useAccept } from "./useAccept";
import type { DecisionDraft } from "./decisions";
// 后端客户端与 Toast 是 hook 的外部副作用边界,单测一律 mock。
const post = vi.fn();
const toast = vi.fn();
vi.mock("@/lib/api/client", () => ({ api: { POST: (...a: unknown[]) => post(...a) } }));
vi.mock("@/components/Toast", () => ({ useToast: () => toast }));
const drafts: readonly DecisionDraft[] = [{ verdict: "accept", note: "" }];
describe("useAccept", () => {
beforeEach(() => {
post.mockReset();
toast.mockReset();
});
afterEach(() => vi.clearAllMocks());
it("初始为 idle、无结果", () => {
const { result } = renderHook(() => useAccept());
expect(result.current.status).toBe("idle");
expect(result.current.result).toBeNull();
});
it("验收成功status=accepted 并回填结果且弹成功 toast", async () => {
const data = { updated: ["digest"] };
post.mockResolvedValue({ data, error: null });
const { result } = renderHook(() => useAccept());
let outcome;
await act(async () => {
outcome = await result.current.accept("p1", 3, "正文", drafts);
});
expect(result.current.status).toBe("accepted");
expect(result.current.result).toEqual(data);
expect(outcome).toEqual({
result: data,
missingIndices: [],
conflictUnresolved: false,
});
expect(toast).toHaveBeenCalledWith("本章已验收", "success");
});
it("409 CONFLICT_UNRESOLVED解析缺判下标且不弹 toast", async () => {
post.mockResolvedValue({
data: null,
error: {
error: {
code: "CONFLICT_UNRESOLVED",
details: { missing_conflict_indices: [0, 2] },
},
},
});
const { result } = renderHook(() => useAccept());
let outcome;
await act(async () => {
outcome = await result.current.accept("p1", 3, "正文", drafts);
});
expect(result.current.status).toBe("error");
expect(outcome).toEqual({
result: null,
missingIndices: [0, 2],
conflictUnresolved: true,
});
expect(toast).not.toHaveBeenCalled();
});
it("CONFLICT_UNRESOLVED 缺 details 时缺判下标回退为空数组", async () => {
post.mockResolvedValue({
data: null,
error: { error: { code: "CONFLICT_UNRESOLVED" } },
});
const { result } = renderHook(() => useAccept());
let outcome;
await act(async () => {
outcome = await result.current.accept("p1", 3, "正文", drafts);
});
expect(outcome).toEqual({
result: null,
missingIndices: [],
conflictUnresolved: true,
});
});
it("其它错误status=error 并弹失败 toast正文未丢", async () => {
post.mockResolvedValue({
data: null,
error: { error: { code: "INTERNAL" } },
});
const { result } = renderHook(() => useAccept());
let outcome;
await act(async () => {
outcome = await result.current.accept("p1", 3, "正文", drafts);
});
expect(result.current.status).toBe("error");
expect(outcome).toEqual({
result: null,
missingIndices: [],
conflictUnresolved: false,
});
expect(toast).toHaveBeenCalledWith("验收失败,请重试(正文未丢失)", "error");
});
it("error 为空但 data 也为空时走通用失败分支", async () => {
post.mockResolvedValue({ data: null, error: undefined });
const { result } = renderHook(() => useAccept());
let outcome;
await act(async () => {
outcome = await result.current.accept("p1", 3, "正文", drafts);
});
expect(result.current.status).toBe("error");
expect(outcome).toEqual({
result: null,
missingIndices: [],
conflictUnresolved: false,
});
expect(toast).toHaveBeenCalledWith("验收失败,请重试(正文未丢失)", "error");
});
});