feat(foreshadow): 写作台本章伏笔主动提醒(前端)
- 新增 chapterForeshadow 纯分类:按当前章分 可回收/待回收/已逾期(启用闲置的 isWindowApproaching) - useChapterForeshadow hook(复用 GET /foreshadow,切章仅重分类不重拉) - 右栏「本章参考」+ ContextDrawer 伏笔页新增「本章伏笔」实时清单, 分色展示 + 跳看板链接 + 空态;标注『按已验收进度』
This commit is contained in:
79
apps/web/lib/foreshadow/useChapterForeshadow.test.ts
Normal file
79
apps/web/lib/foreshadow/useChapterForeshadow.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// @vitest-environment jsdom
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ForeshadowView } from "@/lib/api/types";
|
||||
import { useChapterForeshadow } from "./useChapterForeshadow";
|
||||
|
||||
// 后端客户端是 hook 的外部副作用边界,单测一律 mock。
|
||||
const get = vi.fn();
|
||||
vi.mock("@/lib/api/client", () => ({
|
||||
api: { GET: (...a: unknown[]) => get(...a) },
|
||||
}));
|
||||
|
||||
function makeRow(over: Partial<ForeshadowView>): ForeshadowView {
|
||||
return { code: "F-001", title: "线索", status: "OPEN", ...over } as ForeshadowView;
|
||||
}
|
||||
|
||||
describe("useChapterForeshadow", () => {
|
||||
beforeEach(() => get.mockReset());
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
||||
it("拉取成功后按当前章号分类", async () => {
|
||||
get.mockResolvedValue({
|
||||
data: {
|
||||
foreshadow: [
|
||||
makeRow({ code: "R", status: "OPEN", expected_close_from: 40, expected_close_to: 60 }),
|
||||
makeRow({ code: "P", status: "OPEN", expected_close_from: 80 }),
|
||||
makeRow({ code: "O", status: "OVERDUE" }),
|
||||
],
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useChapterForeshadow("p1", 50));
|
||||
|
||||
await waitFor(() => expect(result.current.status).toBe("ready"));
|
||||
expect(result.current.groups.recyclable.map((v) => v.code)).toEqual(["R"]);
|
||||
expect(result.current.groups.pending.map((v) => v.code)).toEqual(["P"]);
|
||||
expect(result.current.groups.overdue.map((v) => v.code)).toEqual(["O"]);
|
||||
});
|
||||
|
||||
it("拉取失败 → status=error、空分组", async () => {
|
||||
get.mockResolvedValue({ data: null, error: { error: {} } });
|
||||
|
||||
const { result } = renderHook(() => useChapterForeshadow("p1", 3));
|
||||
|
||||
await waitFor(() => expect(result.current.status).toBe("error"));
|
||||
expect(result.current.groups.recyclable).toEqual([]);
|
||||
expect(result.current.groups.pending).toEqual([]);
|
||||
expect(result.current.groups.overdue).toEqual([]);
|
||||
});
|
||||
|
||||
it("切章不重新请求,仅重新分类", async () => {
|
||||
get.mockResolvedValue({
|
||||
data: {
|
||||
foreshadow: [
|
||||
makeRow({ code: "W", status: "OPEN", expected_close_from: 40, expected_close_to: 60 }),
|
||||
],
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ ch }: { ch: number }) => useChapterForeshadow("p1", ch),
|
||||
{ initialProps: { ch: 50 } },
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.status).toBe("ready"));
|
||||
expect(result.current.groups.recyclable.map((v) => v.code)).toEqual(["W"]);
|
||||
|
||||
// 切到窗口外的章:仍是同一份数据,重新分类进 pending,且未再次 GET。
|
||||
rerender({ ch: 10 });
|
||||
await waitFor(() =>
|
||||
expect(result.current.groups.pending.map((v) => v.code)).toEqual(["W"]),
|
||||
);
|
||||
expect(result.current.groups.recyclable).toEqual([]);
|
||||
expect(get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user