import { beforeEach, describe, expect, it, vi } from "vitest"; const findUnique = vi.hoisted(() => vi.fn()); const findMany = vi.hoisted(() => vi.fn()); const update = vi.hoisted(() => vi.fn()); const create = vi.hoisted(() => vi.fn()); const queryRaw = vi.hoisted(() => vi.fn()); const executeRaw = vi.hoisted(() => vi.fn()); const catalogDelete = vi.hoisted(() => vi.fn()); const catalogUpdateMany = vi.hoisted(() => vi.fn()); vi.mock("@/lib/prisma", () => ({ prisma: { catalogPages: { findUnique, findMany, update, create, delete: catalogDelete, updateMany: catalogUpdateMany, }, $queryRaw: queryRaw, $executeRaw: executeRaw, }, })); vi.mock("@/env", () => ({ env: {} })); import type { TreeNode } from "@/types/catalog"; import { buildNestedTree, createPage, deletePage, getAncestors, movePage, toInt, } from "./catalog-tree"; beforeEach(() => { vi.clearAllMocks(); }); describe("toInt", () => { it("converts valid numbers", () => { expect(toInt(42)).toBe(42); expect(toInt("42")).toBe(42); }); it("uses fallback for invalid", () => { expect(toInt("abc", -1)).toBe(-1); expect(toInt(NaN, 0)).toBe(0); }); it("defaults fallback to 0", () => { expect(toInt("abc")).toBe(0); }); }); describe("buildNestedTree", () => { const flat: TreeNode[] = [ { id: 1, caption: "Root", parentId: -1, depth: 0, orderNum: 1, enabled: "1", visible: "1", iconImage: 0, iconColor: 0, pageLayout: "default_3x3", childCount: 2, itemCount: 0, }, { id: 2, caption: "Child", parentId: 1, depth: 1, orderNum: 1, enabled: "1", visible: "1", iconImage: 0, iconColor: 0, pageLayout: "default_3x3", childCount: 1, itemCount: 5, }, { id: 3, caption: "Grandchild", parentId: 2, depth: 2, orderNum: 1, enabled: "1", visible: "1", iconImage: 0, iconColor: 0, pageLayout: "default_3x3", childCount: 0, itemCount: 0, }, ]; it("builds nested tree from flat nodes", () => { const tree = buildNestedTree(flat); expect(tree).toHaveLength(1); expect(tree[0].id).toBe(1); expect(tree[0].children).toHaveLength(1); expect(tree[0].children[0].children[0].id).toBe(3); }); it("returns empty array for empty input", () => { expect(buildNestedTree([])).toEqual([]); }); it("uses custom rootParentId", () => { expect(buildNestedTree(flat, 999)).toHaveLength(0); }); }); describe("createPage", () => { it("creates page with default values", async () => { create.mockResolvedValue({ id: 1 }); await createPage({ parentId: 0, caption: "Test" }); const data = create.mock.calls[0][0].data; expect(data.caption).toBe("Test"); expect(data.pageLayout).toBe("default_3x3"); expect(data.visible).toBe("1"); expect(data.enabled).toBe("1"); expect(data.clubOnly).toBe("0"); expect(data.minRank).toBe(1); }); it("truncates captionSave to 25 chars", async () => { create.mockResolvedValue({ id: 1 }); await createPage({ parentId: 0, caption: "a".repeat(50) }); expect(create.mock.calls[0][0].data.captionSave).toBe("a".repeat(25)); }); it("converts boolean visible/enabled to string", async () => { create.mockResolvedValue({ id: 1 }); await createPage({ parentId: 0, caption: "x", visible: false, enabled: false, }); expect(create.mock.calls[0][0].data.visible).toBe("0"); expect(create.mock.calls[0][0].data.enabled).toBe("0"); }); }); describe("movePage", () => { it("throws on circular hierarchy", async () => { findUnique.mockResolvedValue({ parentId: 1 }); await expect(movePage(1, 2)).rejects.toThrow("circular hierarchy"); }); it("allows valid move to root", async () => { update.mockResolvedValue({}); await movePage(1, 0); expect(update).toHaveBeenCalledWith({ where: { id: 1 }, data: { parentId: 0 }, }); }); it("allows valid move to other parent", async () => { findUnique.mockResolvedValue({ parentId: 0 }); update.mockResolvedValue({}); await movePage(1, 2); expect(update).toHaveBeenCalledWith({ where: { id: 1 }, data: { parentId: 2 }, }); }); }); describe("getAncestors", () => { it("returns breadcrumb from root to page", async () => { findUnique .mockResolvedValueOnce({ id: 3, caption: "Grandchild", parentId: 2 }) .mockResolvedValueOnce({ id: 2, caption: "Child", parentId: 1 }) .mockResolvedValueOnce({ id: 1, caption: "Root", parentId: 0 }); const ancestors = await getAncestors(3); expect(ancestors).toHaveLength(3); expect(ancestors[0].caption).toBe("Root"); expect(ancestors[1].caption).toBe("Child"); expect(ancestors[2].caption).toBe("Grandchild"); }); it("stops when page not found", async () => { findUnique.mockResolvedValue(null); expect(await getAncestors(999)).toEqual([]); }); }); describe("deletePage", () => { it("reparent mode moves children to parent", async () => { findUnique.mockResolvedValue({ parentId: 1 }); catalogDelete.mockResolvedValue({}); catalogUpdateMany.mockResolvedValue({ count: 2 }); const result = await deletePage(5, "reparent"); expect(result).toEqual({ deletedPages: 1, movedChildren: 2 }); expect(catalogUpdateMany).toHaveBeenCalledWith({ where: { parentId: 5 }, data: { parentId: 1 }, }); }); });