import { describe, expect, it } from "vitest"; import { canDropFolder, canMoveFolderTo, descendantIds, folderColor, movable } from "../folderMove"; import type { Id, Mailbox } from "@/jmap/types"; const mb = (id: string, name: string, parentId: string | null, role: Mailbox["role"] = null): Mailbox => ({ id, name, parentId, role, sortOrder: 0, totalEmails: 0, unreadEmails: 0, totalThreads: 0, unreadThreads: 0, isSubscribed: true, myRights: {} as Mailbox["myRights"] }); /** root ── Work ── Clients ── EU * └─ Archive (role) * └─ Inbox (role) */ const tree: Record = Object.fromEntries([ mb("inbox", "Inbox", null, "inbox"), mb("arch", "Archive", null, "archive"), mb("work", "Work", null), mb("clients", "Clients", "work"), mb("eu", "EU", "clients"), mb("news", "Newsletters", null), ].map((m) => [m.id, m])); describe("movable", () => { it("refuses folders the server gave a role", () => { expect(movable(tree.inbox!)).toBe(false); expect(movable(tree.arch!)).toBe(false); expect(movable(tree.work!)).toBe(true); }); }); describe("descendantIds", () => { it("finds the whole subtree, not just the children", () => { expect([...descendantIds(tree, "work")].sort()).toEqual(["clients", "eu"]); expect([...descendantIds(tree, "eu")]).toEqual([]); }); }); describe("canDropFolder", () => { it("allows a plain move into another folder", () => { expect(canDropFolder(tree, "news", "work")).toBe(true); expect(canDropFolder(tree, "eu", "news")).toBe(true); }); it("allows a move into a role folder, which may hold subfolders", () => { expect(canDropFolder(tree, "news", "arch")).toBe(true); }); it("refuses to move a folder into itself or its own subtree", () => { expect(canDropFolder(tree, "work", "work")).toBe(false); expect(canDropFolder(tree, "work", "clients")).toBe(false); expect(canDropFolder(tree, "work", "eu")).toBe(false); // grandchild, not just child }); it("refuses a move to the parent it already has", () => { expect(canDropFolder(tree, "clients", "work")).toBe(false); }); it("refuses to move a role folder anywhere", () => { expect(canDropFolder(tree, "inbox", "work")).toBe(false); expect(canDropFolder(tree, "arch", null)).toBe(false); }); it("handles the root: allowed from a parent, refused when already there", () => { expect(canDropFolder(tree, "eu", null)).toBe(true); expect(canDropFolder(tree, "news", null)).toBe(false); }); it("refuses a target that does not exist", () => { expect(canDropFolder(tree, "news", "gone")).toBe(false); expect(canDropFolder(tree, "gone", "work")).toBe(false); }); }); describe("canMoveFolderTo", () => { const rights = (r: Partial) => ({ mayRename: true, mayCreateChild: true, ...r }) as Mailbox["myRights"]; const owned: Record = Object.fromEntries(Object.values(tree).map((m) => [m.id, { ...m, myRights: rights({}) }])); it("agrees with a drop when every right is granted", () => { expect(canMoveFolderTo(owned, "news", "work")).toBe(true); expect(canMoveFolderTo(owned, "eu", null)).toBe(true); expect(canMoveFolderTo(owned, "work", "eu")).toBe(false); expect(canMoveFolderTo(owned, "news", null)).toBe(false); }); it("refuses a folder the user may not rename, top level included", () => { const locked = { ...owned, eu: { ...owned.eu!, myRights: rights({ mayRename: false }) } }; expect(canMoveFolderTo(locked, "eu", "news")).toBe(false); expect(canMoveFolderTo(locked, "eu", null)).toBe(false); }); it("refuses a destination that may not hold new subfolders", () => { const closed = { ...owned, work: { ...owned.work!, myRights: rights({ mayCreateChild: false }) } }; expect(canMoveFolderTo(closed, "news", "work")).toBe(false); expect(canMoveFolderTo(closed, "eu", null)).toBe(true); }); }); describe("folderColor", () => { it("returns the color chosen for that folder, and null for the rest", () => { const colors = { work: "#7c3aed" }; expect(folderColor(colors, "work")).toBe("#7c3aed"); expect(folderColor(colors, "news")).toBeNull(); expect(folderColor({}, "work")).toBeNull(); }); it("is keyed by id, so a renamed folder keeps its color", () => { // The id is stable across a rename; the name and path are not. expect(folderColor({ mb1: "#0f766e" }, "mb1")).toBe("#0f766e"); }); });