Files
writer-work-flow/apps/web/lib/workbench/useRefine.test.ts
Yaojia Wang 2f0b84191a feat(frontend): 编辑器内「润色 / 再沟通」——选段回炉 + 追加意见迭代出版本栈,接受回填
Phase 1(写作工作台重构):编辑器选中一段即可「润色选段」,复用同步 /refine 回炉
(只读、不写库,守不变量 #3);不满意可「再沟通」——以上一版产出为输入 + 作者新意见
再改一版,形成版本栈(useRefine,已单测)。接受回填按内容重锚落回草稿,原文已变则
提示重选、绝不盲替换(applyRefinement,已单测)。Editor 上报选区,结果以内联卡片展示。
2026-07-07 18:26:47 +02:00

132 lines
4.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 }));
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());
await act(async () => {
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);
});
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();
});
});