// @vitest-environment jsdom import { act, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { RuleView } from "@/lib/api/types"; import { useRules } from "./useRules"; const post = vi.fn(); const del = vi.fn(); const toast = vi.fn(); vi.mock("@/lib/api/client", () => ({ api: { POST: (...a: unknown[]) => post(...a), DELETE: (...a: unknown[]) => del(...a) }, })); vi.mock("@/components/Toast", () => ({ useToast: () => toast })); const seed: RuleView[] = [{ id: "r0", level: "global", content: "已有规则" }]; describe("useRules", () => { beforeEach(() => { post.mockReset(); del.mockReset(); toast.mockReset(); }); afterEach(() => vi.clearAllMocks()); it("初始 items = 传入值,adding=false、deletingId=null", () => { const { result } = renderHook(() => useRules(seed)); expect(result.current.items).toEqual(seed); expect(result.current.adding).toBe(false); expect(result.current.deletingId).toBeNull(); }); it("add 空白正文:弹错、不请求、返回 false", async () => { const { result } = renderHook(() => useRules(seed)); let ok = true; await act(async () => { ok = await result.current.add("p1", "global", " "); }); expect(ok).toBe(false); expect(post).not.toHaveBeenCalled(); expect(toast).toHaveBeenCalledWith("请填写规则正文。", "error"); }); it("add 成功:用服务端权威行替换乐观行、弹成功、返回 true", async () => { const authoritative: RuleView = { id: "r1", level: "global", content: "新规则" }; post.mockResolvedValue({ data: authoritative, error: null }); const { result } = renderHook(() => useRules(seed)); let ok = false; await act(async () => { ok = await result.current.add("p1", "global", "新规则"); }); expect(ok).toBe(true); expect(result.current.items).toEqual([...seed, authoritative]); expect(result.current.adding).toBe(false); expect(toast).toHaveBeenCalledWith("已新增规则", "success"); }); it("add 后端 error:回滚到快照、弹错、返回 false", async () => { post.mockResolvedValue({ data: null, error: { detail: "boom" } }); const { result } = renderHook(() => useRules(seed)); let ok = true; await act(async () => { ok = await result.current.add("p1", "global", "新规则"); }); expect(ok).toBe(false); expect(result.current.items).toEqual(seed); expect(toast).toHaveBeenCalledWith("新增规则失败,请稍后重试。", "error"); }); it("remove 成功:乐观移除、弹成功、返回 true", async () => { del.mockResolvedValue({ error: null }); const { result } = renderHook(() => useRules(seed)); let ok = false; await act(async () => { ok = await result.current.remove("p1", "r0"); }); expect(ok).toBe(true); expect(result.current.items).toEqual([]); expect(toast).toHaveBeenCalledWith("已删除规则", "success"); }); it("add 进行中:adding=true 而 deletingId 仍为 null(互不冻结)", async () => { let resolvePost: (v: { data: RuleView; error: null }) => void = () => {}; post.mockReturnValue( new Promise((res) => { resolvePost = res; }), ); const { result } = renderHook(() => useRules(seed)); let done: Promise = Promise.resolve(false); act(() => { done = result.current.add("p1", "global", "新规则"); }); expect(result.current.adding).toBe(true); expect(result.current.deletingId).toBeNull(); await act(async () => { resolvePost({ data: { id: "r1", level: "global", content: "新规则" }, error: null }); await done; }); expect(result.current.adding).toBe(false); }); it("remove 进行中:deletingId=目标 id 而 adding 仍为 false(只冻结该行)", async () => { let resolveDel: (v: { error: null }) => void = () => {}; del.mockReturnValue( new Promise((res) => { resolveDel = res; }), ); const { result } = renderHook(() => useRules(seed)); let done: Promise = Promise.resolve(false); act(() => { done = result.current.remove("p1", "r0"); }); expect(result.current.deletingId).toBe("r0"); expect(result.current.adding).toBe(false); await act(async () => { resolveDel({ error: null }); await done; }); expect(result.current.deletingId).toBeNull(); }); it("remove 后端 error:回滚、弹错、返回 false", async () => { del.mockResolvedValue({ error: { detail: "boom" } }); const { result } = renderHook(() => useRules(seed)); let ok = true; await act(async () => { ok = await result.current.remove("p1", "r0"); }); expect(ok).toBe(false); expect(result.current.items).toEqual(seed); expect(toast).toHaveBeenCalledWith("删除规则失败,请稍后重试。", "error"); }); });