import { describe, expect, test } from "bun:test"; import { buildImageToolbarTools } from "@/components/canvas/canvas-image-toolbar-tools"; import { resolveNodeToolbarPlacement, resolveToolbarTools, type ToolContext, type ToolbarHandlers } from "@/lib/canvas/tool-registry"; import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas"; function createNodeContext(node: CanvasNodeData): ToolContext { return { selectedCount: 0, selectedNodeTypes: new Set(), selectedVideoCount: 0, canvasTool: "move", workspaceMode: "professional", isProjectLinked: false, canUndo: false, canRedo: false, node, nodeMetadata: node.metadata, extractingVideoFrames: false, extractingAudio: false, trimmingVideo: false, mergingVideos: false, addPanelOpen: false, appearancePanelOpen: false, settingsPanelOpen: false, handlers: {} as ToolbarHandlers, }; } describe("canvas node toolbar model", () => { test("preserves empty, simple and busy video states", () => { const ctx = createNodeContext({ id: "video", type: CanvasNodeType.Video, title: "视频", position: { x: 0, y: 0 }, width: 320, height: 180, metadata: {} }); const emptyTools = resolveToolbarTools("node-hover", ctx, null); expect(emptyTools.some((tool) => tool.id === "extractAudio")).toBe(false); expect(resolveNodeToolbarPlacement(emptyTools.find((tool) => tool.id === "uploadVideo")!, ctx).group).toBe("primary"); ctx.nodeMetadata = { content: "video.mp4" }; ctx.extractingAudio = true; const audio = resolveToolbarTools("node-hover", ctx, null).find((tool) => tool.id === "extractAudio")!; expect(audio.disabled?.(ctx)).toBe(true); ctx.workspaceMode = "simple"; expect(resolveToolbarTools("node-hover", ctx, null).some((tool) => tool.id === "extractAudio")).toBe(false); }); test("image menu actions remain lazy and keep the original node", () => { const node: CanvasNodeData = { id: "image", type: CanvasNodeType.Image, title: "图片", position: { x: 0, y: 0 }, width: 320, height: 180, metadata: { content: "image.png", freeResize: true } }; const received: CanvasNodeData[] = []; const tools = buildImageToolbarTools(node, { onCrop: (target: CanvasNodeData) => received.push(target) } as never); expect(received).toEqual([]); tools.find((tool) => tool.id === "crop")!.onClick(); expect(received).toEqual([node]); expect(tools.find((tool) => tool.id === "resize")?.active).toBe(false); }); test("keeps video primary actions compact and moves secondary processing into a menu", () => { const ctx = createNodeContext({ id: "video", type: CanvasNodeType.Video, title: "视频", position: { x: 0, y: 0 }, width: 320, height: 180, metadata: { content: "video.mp4" } }); const tools = resolveToolbarTools("node-hover", ctx, null); const groups = new Map(tools.map((tool) => [tool.id, resolveNodeToolbarPlacement(tool, ctx).group])); expect([...groups].filter(([, group]) => group === "primary").map(([id]) => id)).toEqual(["trimRegenerate", "subtitles"]); expect([...groups].filter(([, group]) => group === "process").map(([id]) => id)).toEqual(["extractFrames", "extractAudio"]); expect(groups.get("download")).toBe("utility"); expect(groups.get("timeline")).toBe("workspace"); expect(groups.get("uploadVideo")).toBe("more"); expect(groups.get("delete")).toBe("more"); expect(groups.get("node-lock")).toBe("more"); }); test("uses the redesigned video terminology from the registry", () => { const ctx = createNodeContext({ id: "video", type: CanvasNodeType.Video, title: "视频", position: { x: 0, y: 0 }, width: 320, height: 180, metadata: { content: "video.mp4" } }); const tools = new Map(resolveToolbarTools("node-hover", ctx, null).map((tool) => [tool.id, tool])); const displayLabel = (id: string) => { const value = tools.get(id)?.displayLabel; return typeof value === "function" ? value(ctx) : value; }; expect(displayLabel("subtitles")).toBe("字幕"); expect(displayLabel("timeline")).toBe("进入剪辑"); expect(displayLabel("extractFrames")).toBe("提取画面"); expect(displayLabel("extractAudio")).toBe("提取音频"); }); test("image-only tools carry presentation metadata instead of relying on component ID lists", () => { const node: CanvasNodeData = { id: "image", type: CanvasNodeType.Image, title: "图片", position: { x: 0, y: 0 }, width: 320, height: 180, metadata: { content: "image.png" } }; const tools = buildImageToolbarTools(node, {} as never); expect(tools.find((tool) => tool.id === "angle")?.group).toBe("viewpoint"); expect(tools.find((tool) => tool.id === "maskEdit")?.group).toBe("primary"); expect(tools.some((tool) => tool.id === "superResolve")).toBe(false); expect(tools.find((tool) => tool.id === "emotion")?.group).toBe("portrait"); expect(tools.find((tool) => tool.id === "upscale")?.description).toContain("不是 AI 超分"); expect(tools.find((tool) => tool.id === "crop")?.section).toBe("构图与尺寸"); expect(tools.find((tool) => tool.id === "resize")?.active).toBe(true); expect(tools.find((tool) => tool.id === "copyPrompt")?.group).toBe("more"); expect(tools.find((tool) => tool.id === "previs")?.group).toBe("primary"); expect(tools.find((tool) => tool.id === "previs")?.description).toContain("Agent"); }); });