Files
writer-work-flow/apps/web/lib/workbench/useRefine.test.ts
Yaojia Wang 59f2f60a7e refactor(frontend): 四生成 hook 结束后额外返回产物(AC-3 留痕前置)
useRefine.refine/recommunicate→RefineVersion|null、useContinue.generate→string|null、
useChapterRewrite.send→RewriteVersion|null、useGenerator.generate→GeneratorArtifact|null。
向后兼容(既有调用忽略返回值不受影响);各 hook 成功用例补返回值断言。
面板据此 if(artifact) 才 append,空候选/错误路径不留痕。
2026-07-09 17:15:49 +02:00

139 lines
5.1 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 }));
const REFINE_PATH = "/projects/{project_id}/chapters/{chapter_no}/refine";
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.versions).toEqual([]);
expect(result.current.latest).toBeNull();
expect(result.current.original).toBeNull();
});
it("润色:调 /refine压入一版original=选中原文status=ready", async () => {
post.mockResolvedValue({
data: { original: "原文段", refined: "润色后的段落" },
error: null,
});
const { result } = renderHook(() => useRefine());
let returned: unknown;
await act(async () => {
returned = await result.current.refine("p1", 3, "原文段");
});
expect(post).toHaveBeenCalledWith(REFINE_PATH, {
params: { path: { project_id: "p1", chapter_no: 3 } },
body: { segment: "原文段" },
});
expect(result.current.status).toBe("ready");
expect(result.current.original).toBe("原文段");
expect(result.current.latest).toEqual({
segment: "原文段",
instruction: "",
refined: "润色后的段落",
});
expect(result.current.versions).toHaveLength(1);
// 追加断言refine 返回本版产物(供 RefinePanel 结束后留痕)。
expect(returned).toEqual({
segment: "原文段",
instruction: "",
refined: "润色后的段落",
});
});
it("润色带指令instruction 透传后端", async () => {
post.mockResolvedValue({ data: { original: "s", refined: "r" }, error: null });
const { result } = renderHook(() => useRefine());
await act(async () => {
await result.current.refine("p1", 1, "原文", "更口语一点");
});
expect(post).toHaveBeenCalledWith(REFINE_PATH, {
params: { path: { project_id: "p1", chapter_no: 1 } },
body: { segment: "原文", instruction: "更口语一点" },
});
});
it("再沟通:以上一版产出为输入 + 作者新意见压入新版original 不变", async () => {
post
.mockResolvedValueOnce({ data: { original: "原文", refined: "第一版" }, error: null })
.mockResolvedValueOnce({ data: { original: "第一版", refined: "第二版" }, error: null });
const { result } = renderHook(() => useRefine());
await act(async () => {
await result.current.refine("p1", 2, "原文");
});
await act(async () => {
await result.current.recommunicate("p1", 2, "再收紧节奏");
});
expect(post).toHaveBeenLastCalledWith(REFINE_PATH, {
params: { path: { project_id: "p1", chapter_no: 2 } },
body: { segment: "第一版", instruction: "再收紧节奏" },
});
expect(result.current.versions).toHaveLength(2);
expect(result.current.original).toBe("原文");
expect(result.current.latest?.refined).toBe("第二版");
});
it("无历史版本时再沟通为空操作(不打后端)", async () => {
const { result } = renderHook(() => useRefine());
await act(async () => {
await result.current.recommunicate("p1", 1, "改一下");
});
expect(post).not.toHaveBeenCalled();
});
it("后端返回 errorstatus=error、弹错误 toast、版本栈不变", async () => {
post.mockResolvedValue({ data: null, error: { detail: "配额不足" } });
const { result } = renderHook(() => useRefine());
await act(async () => {
await result.current.refine("p1", 1, "原文");
});
expect(result.current.status).toBe("error");
expect(result.current.versions).toEqual([]);
expect(toast).toHaveBeenCalledWith(expect.any(String), "error");
});
it("请求抛异常status=error、弹网络异常 toast", async () => {
post.mockRejectedValue(new Error("network down"));
const { result } = renderHook(() => useRefine());
await act(async () => {
await result.current.refine("p1", 1, "原文");
});
expect(result.current.status).toBe("error");
expect(toast).toHaveBeenCalledWith("回炉请求异常,请检查网络。", "error");
});
it("reset 清回 idle 与空版本", async () => {
post.mockResolvedValue({ data: { original: "s", refined: "r" }, error: null });
const { result } = renderHook(() => useRefine());
await act(async () => {
await result.current.refine("p1", 1, "s");
});
act(() => result.current.reset());
expect(result.current.status).toBe("idle");
expect(result.current.versions).toEqual([]);
expect(result.current.latest).toBeNull();
});
});