364 lines
10 KiB
TypeScript
364 lines
10 KiB
TypeScript
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",
|
|
},
|
|
]);
|
|
});
|
|
});
|