Files
writer-work-flow/apps/web/lib/style/useRefine.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

126 lines
3.9 KiB
TypeScript
Raw 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 { useRefine } from "./useRefine";
// 后端客户端与 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 }));
describe("useRefine", () => {
beforeEach(() => {
post.mockReset();
toast.mockReset();
});
afterEach(() => vi.clearAllMocks());
it("初始为 idle、无结果", () => {
const { result } = renderHook(() => useRefine());
expect(result.current.status).toBe("idle");
expect(result.current.result).toBeNull();
});
it("回炉成功status=done 并返回新旧 diff", async () => {
post.mockResolvedValue({
data: { original: "旧文", refined: "新文" },
error: null,
});
const { result } = renderHook(() => useRefine());
let outcome: unknown;
await act(async () => {
outcome = await result.current.refine("p1", 3, "旧文", "更紧凑");
});
expect(outcome).toEqual({ original: "旧文", refined: "新文" });
expect(result.current.status).toBe("done");
expect(result.current.result).toEqual({ original: "旧文", refined: "新文" });
expect(toast).not.toHaveBeenCalled();
});
it("成功路径以 trim 后的段提交、可省略空指令", async () => {
post.mockResolvedValue({
data: { original: "段", refined: "改" },
error: null,
});
const { result } = renderHook(() => useRefine());
await act(async () => {
await result.current.refine("p1", 1, " 段 ");
});
expect(post).toHaveBeenCalledWith(
"/projects/{project_id}/chapters/{chapter_no}/refine",
expect.objectContaining({ body: { segment: "段" } }),
);
});
it("LLM_UNAVAILABLEstatus=error 且提示去设置页", async () => {
post.mockResolvedValue({
data: null,
error: { error: { code: "LLM_UNAVAILABLE" } },
});
const { result } = renderHook(() => useRefine());
let outcome: unknown;
await act(async () => {
outcome = await result.current.refine("p1", 2, "段");
});
expect(outcome).toBeNull();
expect(result.current.status).toBe("error");
expect(toast).toHaveBeenCalledWith(
"未配置提供商,请先去设置页连一家。",
"error",
);
});
it("其余后端错误status=error 且弹通用回炉失败 toast", async () => {
post.mockResolvedValue({
data: null,
error: { error: { code: "INTERNAL" } },
});
const { result } = renderHook(() => useRefine());
await act(async () => {
await result.current.refine("p1", 2, "段");
});
expect(result.current.status).toBe("error");
expect(toast).toHaveBeenCalledWith("回炉失败,请稍后重试。", "error");
});
it("请求抛异常status=error 且弹网络异常 toast", async () => {
post.mockRejectedValue(new Error("network down"));
const { result } = renderHook(() => useRefine());
let outcome: unknown;
await act(async () => {
outcome = await result.current.refine("p1", 2, "段");
});
expect(outcome).toBeNull();
expect(result.current.status).toBe("error");
expect(toast).toHaveBeenCalledWith("回炉请求异常,请检查网络。", "error");
});
it("reset 清回 idle 与空结果", async () => {
post.mockResolvedValue({
data: { original: "a", refined: "b" },
error: null,
});
const { result } = renderHook(() => useRefine());
await act(async () => {
await result.current.refine("p1", 1, "a");
});
act(() => result.current.reset());
expect(result.current.status).toBe("idle");
expect(result.current.result).toBeNull();
});
});