// @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 { 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); }); });