Files
writer-work-flow/apps/web/lib/rules/useRules.test.ts

138 lines
4.9 KiB
TypeScript
Raw Permalink 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 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<boolean> = 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<boolean> = 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");
});
});