import { expect, test } from "@playwright/test"; import { attachCanvasNodesToGroup, collectCanvasGroupDescendantIds, detachCanvasGroups, duplicateCanvasNodeSelection, duplicateCanvasNodeTree, expandCanvasGroupSelection, getCanvasGroupBounds, isCanvasGroupNode, isCanvasStoryboardGroupNode, mergeCanvasStoryboardGroups, } from "@/lib/canvas/grouping"; const childA = { draftValues: { prompt: "A" }, height: 525, id: "image-a", width: 350, x: 621, y: 267.5, }; const childB = { draftValues: { prompt: "B" }, height: 200, id: "text-b", width: 350, x: 1105.75, y: 272, }; test.describe("video canvas grouping", () => { test("uses the observed group title and content padding geometry", () => { expect(getCanvasGroupBounds([childA, childB])).toEqual({ height: 601, width: 874.75, x: 601, y: 211.5, }); }); test("attaches children and detaches them without losing their draft values", () => { const group = { draftValues: { action: "CANVAS_GROUP", nodeType: "group" }, height: 601, id: "group-1", width: 874.75, x: 601, y: 211.5, }; const attached = attachCanvasNodesToGroup( [group, childA, childB], group.id, [childA.id, childB.id], ); expect(isCanvasGroupNode(attached[0])).toBe(true); expect(attached[0].draftValues).toMatchObject({ childNodeIds: ["image-a", "text-b"], }); expect(attached[1].draftValues).toEqual({ parentGroupId: "group-1", prompt: "A" }); const detached = detachCanvasGroups(attached, [group.id]); expect(detached.detachedChildIds).toEqual(["image-a", "text-b"]); expect(detached.nodes.map((node) => node.id)).toEqual(["image-a", "text-b"]); expect(detached.nodes[0].draftValues).toEqual({ prompt: "A" }); }); test("expands nested groups for drag and destructive operations", () => { const nodes = [ { draftValues: { action: "CANVAS_GROUP", childNodeIds: ["nested", "image-a"], nodeType: "group", }, height: 100, id: "outer", width: 100, x: 0, y: 0, }, { draftValues: { action: "CANVAS_GROUP", childNodeIds: ["text-b"], nodeType: "group", }, height: 80, id: "nested", width: 80, x: 10, y: 10, }, childA, childB, ]; expect(collectCanvasGroupDescendantIds(nodes, ["outer"])).toEqual([ "nested", "image-a", "text-b", ]); expect(expandCanvasGroupSelection(nodes, ["outer"])).toEqual([ "outer", "nested", "image-a", "text-b", ]); }); test("merges storyboard groups into one ordinary group without changing their children", () => { const storyboardA = { draftValues: { action: "CANVAS_STORYBOARD_GROUP", childNodeIds: ["shot-a", "shot-b"], nodeType: "group", }, height: 120, id: "storyboard-a", title: "分镜图 · A", width: 240, x: -20, y: -56, }; const storyboardB = { draftValues: { action: "CANVAS_GROUP", childNodeIds: ["shot-c", "shot-d"], groupType: "storyboard", nodeType: "group", }, height: 120, id: "storyboard-b", title: "分镜图 · B", width: 240, x: -20, y: 94, }; const shotNodes = [ { id: "shot-a", x: 0, y: 0 }, { id: "shot-b", x: 120, y: 0 }, { id: "shot-c", x: 0, y: 150 }, { id: "shot-d", x: 120, y: 150 }, ].map((node) => ({ ...node, draftValues: { parentGroupId: node.id < "shot-c" ? storyboardA.id : storyboardB.id, prompt: node.id, }, height: 100, title: node.id, width: 100, })); const unrelatedNode = { draftValues: { prompt: "outside" }, height: 80, id: "outside", title: "outside", width: 80, x: 400, y: 0, }; const result = mergeCanvasStoryboardGroups({ createGroup: ({ bounds, childNodeIds }) => ({ ...bounds, draftValues: { action: "CANVAS_GROUP", childNodeIds, nodeType: "group", }, id: "merged-group", title: `分组 ${childNodeIds.length} 个节点`, }), edges: [ { id: "group-edge", source: storyboardA.id, target: unrelatedNode.id }, { id: "child-edge", source: "shot-a", target: "shot-b" }, { id: "cross-edge", source: "shot-b", target: "shot-c" }, ], nodes: [storyboardA, storyboardB, ...shotNodes, unrelatedNode], selectedNodeIds: [storyboardB.id, storyboardA.id], }); expect(isCanvasStoryboardGroupNode(storyboardA)).toBe(true); expect(isCanvasStoryboardGroupNode(storyboardB)).toBe(true); expect(result?.sourceGroupIds).toEqual([storyboardB.id, storyboardA.id]); expect(result?.childNodeIds).toEqual([ "shot-c", "shot-d", "shot-a", "shot-b", ]); expect(result?.group).toMatchObject({ height: 326, id: "merged-group", title: "分组 4 个节点", width: 260, x: -20, y: -56, }); expect(result?.nodes.map((node) => node.id)).toEqual([ "shot-a", "shot-b", "shot-c", "shot-d", "outside", "merged-group", ]); expect(result?.nodes.slice(0, 4).map((node) => node.draftValues)).toEqual([ { parentGroupId: "merged-group", prompt: "shot-a" }, { parentGroupId: "merged-group", prompt: "shot-b" }, { parentGroupId: "merged-group", prompt: "shot-c" }, { parentGroupId: "merged-group", prompt: "shot-d" }, ]); expect(result?.nodes.at(-1)?.draftValues).toMatchObject({ childNodeIds: ["shot-c", "shot-d", "shot-a", "shot-b"], }); expect(result?.edges).toEqual([ { id: "child-edge", source: "shot-a", target: "shot-b" }, { id: "cross-edge", source: "shot-b", target: "shot-c" }, ]); }); test("does not merge ordinary groups", () => { const ordinaryGroup = { draftValues: { action: "CANVAS_GROUP", childNodeIds: [childA.id, childB.id], nodeType: "group", }, height: 601, id: "ordinary-group", title: "整组执行", width: 874.75, x: 601, y: 211.5, }; expect( mergeCanvasStoryboardGroups({ createGroup: () => ordinaryGroup, edges: [], nodes: [ordinaryGroup, childA, childB], selectedNodeIds: [ordinaryGroup.id, childA.id, childB.id], }), ).toBeNull(); }); test("duplicates a whole group tree without retaining original relationships", () => { let nodeIndex = 0; let edgeIndex = 0; const group = { draftValues: { action: "CANVAS_GROUP", childNodeIds: [childA.id, childB.id], nodeType: "group", }, height: 601, id: "group-1", title: "整组执行", width: 874.75, x: 601, y: 211.5, }; const attachedChildren = attachCanvasNodesToGroup( [{ ...childA, title: "参考图" }, { ...childB, title: "提示词" }], group.id, [childA.id, childB.id], ); const result = duplicateCanvasNodeTree({ createEdgeId: () => `edge-copy-${++edgeIndex}`, createNodeId: () => `node-copy-${++nodeIndex}`, edges: [{ id: "edge-1", source: childA.id, target: childB.id }], nodes: [group, ...attachedChildren], offsetY: 100, rootNodeId: group.id, }); expect(result?.rootNodeId).toBe("node-copy-1"); expect(result?.duplicateNodes.map((node) => node.id)).toEqual([ "node-copy-1", "node-copy-2", "node-copy-3", ]); expect(result?.duplicateNodes[0]).toMatchObject({ draftValues: { childNodeIds: ["node-copy-2", "node-copy-3"] }, title: "整组执行 - 副本", y: 311.5, }); expect(result?.duplicateNodes[1].draftValues).toMatchObject({ parentGroupId: "node-copy-1", prompt: "A", }); expect(result?.duplicateEdges).toEqual([ { id: "edge-copy-1", source: "node-copy-2", target: "node-copy-3", }, ]); }); test("detaches a copied child from its original group", () => { const attachedChild = attachCanvasNodesToGroup( [{ ...childA, title: "参考图" }], "group-1", [childA.id], )[0]; const result = duplicateCanvasNodeTree({ createEdgeId: () => "unused-edge", createNodeId: () => "image-copy", edges: [], nodes: [attachedChild], rootNodeId: attachedChild.id, }); expect(result?.duplicateNodes[0].draftValues).toEqual({ prompt: "A" }); }); test("duplicates a selected set without its internal edges for Option drag", () => { let nodeIndex = 0; const result = duplicateCanvasNodeSelection({ createEdgeId: () => "unused-edge", createNodeId: () => `copy-${++nodeIndex}`, edges: [{ id: "edge-1", source: childA.id, target: childB.id }], includeInternalEdges: false, nodes: [ { ...childA, title: "参考图" }, { ...childB, title: "提示词 - 副本" }, ], selectedNodeIds: [childA.id, childB.id], }); expect(result?.duplicateNodes).toEqual([ expect.objectContaining({ id: "copy-1", title: "参考图 - 副本" }), expect.objectContaining({ id: "copy-2", title: "提示词 - 副本" }), ]); expect(result?.duplicateEdges).toEqual([]); expect(result?.rootNodeIds).toEqual(["copy-1", "copy-2"]); }); test("duplicates selected-set internal edges for Command+Option drag", () => { let nodeIndex = 0; let edgeIndex = 0; const result = duplicateCanvasNodeSelection({ createEdgeId: () => `edge-copy-${++edgeIndex}`, createNodeId: () => `copy-${++nodeIndex}`, edges: [ { id: "internal", source: childA.id, target: childB.id }, { id: "external", source: childB.id, target: "outside" }, ], includeInternalEdges: true, nodes: [ { ...childA, title: "参考图" }, { ...childB, title: "提示词" }, ], selectedNodeIds: [childA.id, childB.id], }); expect(result?.duplicateEdges).toEqual([ { id: "edge-copy-1", source: "copy-1", target: "copy-2", }, ]); }); });