feat(canvas): finalize mixer reconnect swap and related updates
This commit is contained in:
@@ -22,16 +22,12 @@ vi.mock("@/components/canvas/canvas-helpers", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/canvas/canvas-reconnect", () => ({
|
||||
useCanvasReconnectHandlers: () => ({
|
||||
onReconnectStart: vi.fn(),
|
||||
onReconnect: vi.fn(),
|
||||
onReconnectEnd: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
import { useCanvasConnections } from "@/components/canvas/use-canvas-connections";
|
||||
import type { DroppedConnectionTarget } from "@/components/canvas/canvas-helpers";
|
||||
import { nodeTypes } from "@/components/canvas/node-types";
|
||||
import { NODE_CATALOG } from "@/lib/canvas-node-catalog";
|
||||
import { CANVAS_NODE_TEMPLATES } from "@/lib/canvas-node-templates";
|
||||
import { NODE_DEFAULTS, NODE_HANDLE_MAP } from "@/lib/canvas-utils";
|
||||
|
||||
const asCanvasId = (id: string): Id<"canvases"> => id as Id<"canvases">;
|
||||
|
||||
@@ -45,7 +41,10 @@ type HookHarnessProps = {
|
||||
helperResult: DroppedConnectionTarget | null;
|
||||
runCreateEdgeMutation?: ReturnType<typeof vi.fn>;
|
||||
runSplitEdgeAtExistingNodeMutation?: ReturnType<typeof vi.fn>;
|
||||
runRemoveEdgeMutation?: ReturnType<typeof vi.fn>;
|
||||
runSwapMixerInputsMutation?: ReturnType<typeof vi.fn>;
|
||||
showConnectionRejectedToast?: ReturnType<typeof vi.fn>;
|
||||
setEdgesMock?: ReturnType<typeof vi.fn>;
|
||||
nodes?: RFNode[];
|
||||
edges?: RFEdge[];
|
||||
};
|
||||
@@ -54,7 +53,10 @@ function HookHarness({
|
||||
helperResult,
|
||||
runCreateEdgeMutation = vi.fn(async () => undefined),
|
||||
runSplitEdgeAtExistingNodeMutation = vi.fn(async () => undefined),
|
||||
runRemoveEdgeMutation = vi.fn(async () => undefined),
|
||||
runSwapMixerInputsMutation = vi.fn(async () => undefined),
|
||||
showConnectionRejectedToast = vi.fn(),
|
||||
setEdgesMock,
|
||||
nodes: providedNodes,
|
||||
edges: providedEdges,
|
||||
}: HookHarnessProps) {
|
||||
@@ -71,7 +73,7 @@ function HookHarness({
|
||||
const isReconnectDragActiveRef = useRef(false);
|
||||
const pendingConnectionCreatesRef = useRef(new Set<string>());
|
||||
const resolvedRealIdByClientRequestRef = useRef(new Map<string, Id<"nodes">>());
|
||||
const setEdges = vi.fn();
|
||||
const setEdges = setEdgesMock ?? vi.fn();
|
||||
const setEdgeSyncNonce = vi.fn();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -102,7 +104,8 @@ function HookHarness({
|
||||
syncPendingMoveForClientRequest: vi.fn(async () => undefined),
|
||||
runCreateEdgeMutation,
|
||||
runSplitEdgeAtExistingNodeMutation,
|
||||
runRemoveEdgeMutation: vi.fn(async () => undefined),
|
||||
runRemoveEdgeMutation,
|
||||
runSwapMixerInputsMutation,
|
||||
runCreateNodeWithEdgeFromSourceOnlineOnly: vi.fn(async () => "node-1"),
|
||||
runCreateNodeWithEdgeToTargetOnlineOnly: vi.fn(async () => "node-1"),
|
||||
showConnectionRejectedToast,
|
||||
@@ -132,6 +135,47 @@ describe("useCanvasConnections", () => {
|
||||
container = null;
|
||||
});
|
||||
|
||||
it("exposes mixer metadata required for placement and connection defaults", () => {
|
||||
const mixerCatalogEntry = NODE_CATALOG.find((entry) => entry.type === "mixer");
|
||||
const mixerTemplate = CANVAS_NODE_TEMPLATES.find(
|
||||
(template) => (template.type as string) === "mixer",
|
||||
);
|
||||
|
||||
expect(nodeTypes).toHaveProperty("mixer");
|
||||
expect(mixerCatalogEntry).toEqual(
|
||||
expect.objectContaining({
|
||||
type: "mixer",
|
||||
category: "control",
|
||||
implemented: true,
|
||||
}),
|
||||
);
|
||||
expect(mixerTemplate).toEqual(
|
||||
expect.objectContaining({
|
||||
type: "mixer",
|
||||
defaultData: {
|
||||
blendMode: "normal",
|
||||
opacity: 100,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(NODE_HANDLE_MAP.mixer).toEqual({
|
||||
source: "mixer-out",
|
||||
target: "base",
|
||||
});
|
||||
expect(NODE_DEFAULTS.mixer).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
blendMode: "normal",
|
||||
opacity: 100,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("creates an edge when a body drop lands on another node", async () => {
|
||||
const runCreateEdgeMutation = vi.fn(async () => undefined);
|
||||
const showConnectionRejectedToast = vi.fn();
|
||||
@@ -490,6 +534,320 @@ describe("useCanvasConnections", () => {
|
||||
expect(latestHandlersRef.current?.connectionDropMenu).toBeNull();
|
||||
});
|
||||
|
||||
it("allows image-like sources to connect to mixer", async () => {
|
||||
const runCreateEdgeMutation = vi.fn(async () => undefined);
|
||||
const showConnectionRejectedToast = vi.fn();
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
helperResult={{
|
||||
sourceNodeId: "node-source",
|
||||
targetNodeId: "node-target",
|
||||
sourceHandle: undefined,
|
||||
targetHandle: "base",
|
||||
}}
|
||||
nodes={[
|
||||
{ id: "node-source", type: "image", position: { x: 0, y: 0 }, data: {} },
|
||||
{ id: "node-target", type: "mixer", position: { x: 300, y: 200 }, data: {} },
|
||||
]}
|
||||
runCreateEdgeMutation={runCreateEdgeMutation}
|
||||
showConnectionRejectedToast={showConnectionRejectedToast}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
latestHandlersRef.current?.onConnectStart?.(
|
||||
{} as MouseEvent,
|
||||
{
|
||||
nodeId: "node-source",
|
||||
handleId: null,
|
||||
handleType: "source",
|
||||
} as never,
|
||||
);
|
||||
latestHandlersRef.current?.onConnectEnd(
|
||||
{ clientX: 400, clientY: 260 } as MouseEvent,
|
||||
{
|
||||
isValid: false,
|
||||
from: { x: 0, y: 0 },
|
||||
fromNode: { id: "node-source", type: "image" },
|
||||
fromHandle: { id: null, type: "source" },
|
||||
fromPosition: null,
|
||||
to: { x: 400, y: 260 },
|
||||
toHandle: null,
|
||||
toNode: null,
|
||||
toPosition: null,
|
||||
pointer: null,
|
||||
} as never,
|
||||
);
|
||||
});
|
||||
|
||||
expect(runCreateEdgeMutation).toHaveBeenCalledWith({
|
||||
canvasId: "canvas-1",
|
||||
sourceNodeId: "node-source",
|
||||
targetNodeId: "node-target",
|
||||
sourceHandle: undefined,
|
||||
targetHandle: "base",
|
||||
});
|
||||
expect(showConnectionRejectedToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects disallowed source types to mixer", async () => {
|
||||
const runCreateEdgeMutation = vi.fn(async () => undefined);
|
||||
const showConnectionRejectedToast = vi.fn();
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
helperResult={{
|
||||
sourceNodeId: "node-source",
|
||||
targetNodeId: "node-target",
|
||||
sourceHandle: undefined,
|
||||
targetHandle: "base",
|
||||
}}
|
||||
nodes={[
|
||||
{ id: "node-source", type: "video", position: { x: 0, y: 0 }, data: {} },
|
||||
{ id: "node-target", type: "mixer", position: { x: 300, y: 200 }, data: {} },
|
||||
]}
|
||||
runCreateEdgeMutation={runCreateEdgeMutation}
|
||||
showConnectionRejectedToast={showConnectionRejectedToast}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
latestHandlersRef.current?.onConnectStart?.(
|
||||
{} as MouseEvent,
|
||||
{
|
||||
nodeId: "node-source",
|
||||
handleId: null,
|
||||
handleType: "source",
|
||||
} as never,
|
||||
);
|
||||
latestHandlersRef.current?.onConnectEnd(
|
||||
{ clientX: 400, clientY: 260 } as MouseEvent,
|
||||
{
|
||||
isValid: false,
|
||||
from: { x: 0, y: 0 },
|
||||
fromNode: { id: "node-source", type: "video" },
|
||||
fromHandle: { id: null, type: "source" },
|
||||
fromPosition: null,
|
||||
to: { x: 400, y: 260 },
|
||||
toHandle: null,
|
||||
toNode: null,
|
||||
toPosition: null,
|
||||
pointer: null,
|
||||
} as never,
|
||||
);
|
||||
});
|
||||
|
||||
expect(runCreateEdgeMutation).not.toHaveBeenCalled();
|
||||
expect(showConnectionRejectedToast).toHaveBeenCalledWith("mixer-source-invalid");
|
||||
});
|
||||
|
||||
it("rejects a second connection to the same mixer handle", async () => {
|
||||
const runCreateEdgeMutation = vi.fn(async () => undefined);
|
||||
const showConnectionRejectedToast = vi.fn();
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
helperResult={{
|
||||
sourceNodeId: "node-source",
|
||||
targetNodeId: "node-target",
|
||||
sourceHandle: undefined,
|
||||
targetHandle: "base",
|
||||
}}
|
||||
nodes={[
|
||||
{ id: "node-source", type: "asset", position: { x: 0, y: 0 }, data: {} },
|
||||
{ id: "node-target", type: "mixer", position: { x: 300, y: 200 }, data: {} },
|
||||
{ id: "node-image", type: "image", position: { x: -200, y: 40 }, data: {} },
|
||||
]}
|
||||
edges={[
|
||||
{
|
||||
id: "edge-existing-base",
|
||||
source: "node-image",
|
||||
target: "node-target",
|
||||
targetHandle: "base",
|
||||
},
|
||||
]}
|
||||
runCreateEdgeMutation={runCreateEdgeMutation}
|
||||
showConnectionRejectedToast={showConnectionRejectedToast}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
latestHandlersRef.current?.onConnect({
|
||||
source: "node-source",
|
||||
target: "node-target",
|
||||
sourceHandle: null,
|
||||
targetHandle: "base",
|
||||
});
|
||||
});
|
||||
|
||||
expect(runCreateEdgeMutation).not.toHaveBeenCalled();
|
||||
expect(showConnectionRejectedToast).toHaveBeenCalledWith("mixer-handle-incoming-limit");
|
||||
});
|
||||
|
||||
it("allows one incoming edge per mixer handle", async () => {
|
||||
const runCreateEdgeMutation = vi.fn(async () => undefined);
|
||||
const showConnectionRejectedToast = vi.fn();
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
helperResult={{
|
||||
sourceNodeId: "node-source",
|
||||
targetNodeId: "node-target",
|
||||
sourceHandle: undefined,
|
||||
targetHandle: "overlay",
|
||||
}}
|
||||
nodes={[
|
||||
{ id: "node-source", type: "asset", position: { x: 0, y: 0 }, data: {} },
|
||||
{ id: "node-target", type: "mixer", position: { x: 300, y: 200 }, data: {} },
|
||||
{ id: "node-image", type: "image", position: { x: -200, y: 40 }, data: {} },
|
||||
]}
|
||||
edges={[
|
||||
{
|
||||
id: "edge-existing-base",
|
||||
source: "node-image",
|
||||
target: "node-target",
|
||||
targetHandle: "base",
|
||||
},
|
||||
]}
|
||||
runCreateEdgeMutation={runCreateEdgeMutation}
|
||||
showConnectionRejectedToast={showConnectionRejectedToast}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
latestHandlersRef.current?.onConnectStart?.(
|
||||
{} as MouseEvent,
|
||||
{
|
||||
nodeId: "node-source",
|
||||
handleId: null,
|
||||
handleType: "source",
|
||||
} as never,
|
||||
);
|
||||
latestHandlersRef.current?.onConnectEnd(
|
||||
{ clientX: 400, clientY: 260 } as MouseEvent,
|
||||
{
|
||||
isValid: false,
|
||||
from: { x: 0, y: 0 },
|
||||
fromNode: { id: "node-source", type: "asset" },
|
||||
fromHandle: { id: null, type: "source" },
|
||||
fromPosition: null,
|
||||
to: { x: 400, y: 260 },
|
||||
toHandle: null,
|
||||
toNode: null,
|
||||
toPosition: null,
|
||||
pointer: null,
|
||||
} as never,
|
||||
);
|
||||
});
|
||||
|
||||
expect(runCreateEdgeMutation).toHaveBeenCalledWith({
|
||||
canvasId: "canvas-1",
|
||||
sourceNodeId: "node-source",
|
||||
targetNodeId: "node-target",
|
||||
sourceHandle: undefined,
|
||||
targetHandle: "overlay",
|
||||
});
|
||||
expect(showConnectionRejectedToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects a third incoming edge to mixer", async () => {
|
||||
const runCreateEdgeMutation = vi.fn(async () => undefined);
|
||||
const showConnectionRejectedToast = vi.fn();
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
helperResult={{
|
||||
sourceNodeId: "node-source",
|
||||
targetNodeId: "node-target",
|
||||
sourceHandle: undefined,
|
||||
targetHandle: "base",
|
||||
}}
|
||||
nodes={[
|
||||
{ id: "node-source", type: "render", position: { x: 0, y: 0 }, data: {} },
|
||||
{ id: "node-target", type: "mixer", position: { x: 300, y: 200 }, data: {} },
|
||||
{ id: "node-image", type: "image", position: { x: -200, y: 40 }, data: {} },
|
||||
{ id: "node-asset", type: "asset", position: { x: -180, y: 180 }, data: {} },
|
||||
]}
|
||||
edges={[
|
||||
{
|
||||
id: "edge-existing-base",
|
||||
source: "node-image",
|
||||
target: "node-target",
|
||||
targetHandle: "base",
|
||||
},
|
||||
{
|
||||
id: "edge-existing-overlay",
|
||||
source: "node-asset",
|
||||
target: "node-target",
|
||||
targetHandle: "overlay",
|
||||
},
|
||||
]}
|
||||
runCreateEdgeMutation={runCreateEdgeMutation}
|
||||
showConnectionRejectedToast={showConnectionRejectedToast}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
latestHandlersRef.current?.onConnectStart?.(
|
||||
{} as MouseEvent,
|
||||
{
|
||||
nodeId: "node-source",
|
||||
handleId: null,
|
||||
handleType: "source",
|
||||
} as never,
|
||||
);
|
||||
latestHandlersRef.current?.onConnectEnd(
|
||||
{ clientX: 400, clientY: 260 } as MouseEvent,
|
||||
{
|
||||
isValid: false,
|
||||
from: { x: 0, y: 0 },
|
||||
fromNode: { id: "node-source", type: "render" },
|
||||
fromHandle: { id: null, type: "source" },
|
||||
fromPosition: null,
|
||||
to: { x: 400, y: 260 },
|
||||
toHandle: null,
|
||||
toNode: null,
|
||||
toPosition: null,
|
||||
pointer: null,
|
||||
} as never,
|
||||
);
|
||||
});
|
||||
|
||||
expect(runCreateEdgeMutation).not.toHaveBeenCalled();
|
||||
expect(showConnectionRejectedToast).toHaveBeenCalledWith("mixer-incoming-limit");
|
||||
});
|
||||
|
||||
it("ignores onConnectEnd when no connect drag is active", async () => {
|
||||
const runCreateEdgeMutation = vi.fn(async () => undefined);
|
||||
const showConnectionRejectedToast = vi.fn();
|
||||
@@ -535,4 +893,364 @@ describe("useCanvasConnections", () => {
|
||||
expect(showConnectionRejectedToast).not.toHaveBeenCalled();
|
||||
expect(latestHandlersRef.current?.connectionDropMenu).toBeNull();
|
||||
});
|
||||
|
||||
it("passes edgeIdToIgnore during reconnect replacement without client-side old-edge delete", async () => {
|
||||
const runCreateEdgeMutation = vi.fn(async () => undefined);
|
||||
const runRemoveEdgeMutation = vi.fn(async () => undefined);
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
helperResult={null}
|
||||
runCreateEdgeMutation={runCreateEdgeMutation}
|
||||
runRemoveEdgeMutation={runRemoveEdgeMutation}
|
||||
edges={[
|
||||
{
|
||||
id: "edge-1",
|
||||
source: "node-source",
|
||||
target: "node-target",
|
||||
targetHandle: "base",
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const oldEdge = {
|
||||
id: "edge-1",
|
||||
source: "node-source",
|
||||
target: "node-target",
|
||||
targetHandle: "base",
|
||||
} as RFEdge;
|
||||
|
||||
await act(async () => {
|
||||
latestHandlersRef.current?.onReconnectStart();
|
||||
latestHandlersRef.current?.onReconnect(oldEdge, {
|
||||
source: "node-source",
|
||||
target: "node-target",
|
||||
sourceHandle: null,
|
||||
targetHandle: "overlay",
|
||||
});
|
||||
latestHandlersRef.current?.onReconnectEnd({} as MouseEvent, oldEdge);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(runCreateEdgeMutation).toHaveBeenCalledWith({
|
||||
canvasId: "canvas-1",
|
||||
sourceNodeId: "node-source",
|
||||
targetNodeId: "node-target",
|
||||
sourceHandle: undefined,
|
||||
targetHandle: "overlay",
|
||||
edgeIdToIgnore: "edge-1",
|
||||
});
|
||||
expect(runRemoveEdgeMutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not remove old edge when reconnect create fails", async () => {
|
||||
const runCreateEdgeMutation = vi.fn(async () => {
|
||||
throw new Error("incoming limit reached");
|
||||
});
|
||||
const runRemoveEdgeMutation = vi.fn(async () => undefined);
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
helperResult={null}
|
||||
runCreateEdgeMutation={runCreateEdgeMutation}
|
||||
runRemoveEdgeMutation={runRemoveEdgeMutation}
|
||||
edges={[
|
||||
{
|
||||
id: "edge-1",
|
||||
source: "node-source",
|
||||
target: "node-target",
|
||||
targetHandle: "base",
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const oldEdge = {
|
||||
id: "edge-1",
|
||||
source: "node-source",
|
||||
target: "node-target",
|
||||
targetHandle: "base",
|
||||
} as RFEdge;
|
||||
|
||||
await act(async () => {
|
||||
latestHandlersRef.current?.onReconnectStart();
|
||||
latestHandlersRef.current?.onReconnect(oldEdge, {
|
||||
source: "node-source",
|
||||
target: "node-target",
|
||||
sourceHandle: null,
|
||||
targetHandle: "overlay",
|
||||
});
|
||||
latestHandlersRef.current?.onReconnectEnd({} as MouseEvent, oldEdge);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(runCreateEdgeMutation).toHaveBeenCalledTimes(1);
|
||||
expect(runRemoveEdgeMutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("swaps mixer inputs on reconnect when dropping onto occupied opposite handle (base->overlay)", async () => {
|
||||
const runCreateEdgeMutation = vi.fn(async () => undefined);
|
||||
const runRemoveEdgeMutation = vi.fn(async () => undefined);
|
||||
const runSwapMixerInputsMutation = vi.fn(async () => undefined);
|
||||
const showConnectionRejectedToast = vi.fn();
|
||||
const setEdgesMock = vi.fn();
|
||||
|
||||
const initialEdges: RFEdge[] = [
|
||||
{
|
||||
id: "edge-base",
|
||||
source: "node-source-base",
|
||||
target: "node-mixer",
|
||||
targetHandle: "base",
|
||||
},
|
||||
{
|
||||
id: "edge-overlay",
|
||||
source: "node-source-overlay",
|
||||
target: "node-mixer",
|
||||
targetHandle: "overlay",
|
||||
},
|
||||
];
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
helperResult={null}
|
||||
runCreateEdgeMutation={runCreateEdgeMutation}
|
||||
runRemoveEdgeMutation={runRemoveEdgeMutation}
|
||||
runSwapMixerInputsMutation={runSwapMixerInputsMutation}
|
||||
showConnectionRejectedToast={showConnectionRejectedToast}
|
||||
setEdgesMock={setEdgesMock}
|
||||
nodes={[
|
||||
{ id: "node-source-base", type: "image", position: { x: 0, y: 0 }, data: {} },
|
||||
{ id: "node-source-overlay", type: "asset", position: { x: 0, y: 120 }, data: {} },
|
||||
{ id: "node-mixer", type: "mixer", position: { x: 320, y: 120 }, data: {} },
|
||||
]}
|
||||
edges={initialEdges}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const oldEdge = initialEdges[0] as RFEdge;
|
||||
await act(async () => {
|
||||
latestHandlersRef.current?.onReconnectStart();
|
||||
latestHandlersRef.current?.onReconnect(oldEdge, {
|
||||
source: "node-source-base",
|
||||
target: "node-mixer",
|
||||
sourceHandle: null,
|
||||
targetHandle: "overlay",
|
||||
});
|
||||
latestHandlersRef.current?.onReconnectEnd({} as MouseEvent, oldEdge);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(showConnectionRejectedToast).not.toHaveBeenCalled();
|
||||
expect(runCreateEdgeMutation).not.toHaveBeenCalled();
|
||||
expect(runRemoveEdgeMutation).not.toHaveBeenCalled();
|
||||
expect(runSwapMixerInputsMutation).toHaveBeenCalledWith({
|
||||
canvasId: "canvas-1",
|
||||
edgeId: "edge-base",
|
||||
otherEdgeId: "edge-overlay",
|
||||
});
|
||||
|
||||
expect(setEdgesMock).toHaveBeenCalledTimes(1);
|
||||
const applyEdges = setEdgesMock.mock.calls[0]?.[0] as ((edges: RFEdge[]) => RFEdge[]);
|
||||
const swappedEdges = applyEdges(initialEdges);
|
||||
const baseEdge = swappedEdges.find((edge) => edge.id === "edge-base");
|
||||
const overlayEdge = swappedEdges.find((edge) => edge.id === "edge-overlay");
|
||||
expect(baseEdge?.targetHandle).toBe("overlay");
|
||||
expect(overlayEdge?.targetHandle).toBe("base");
|
||||
});
|
||||
|
||||
it("swaps mixer inputs on reconnect when dropping onto occupied opposite handle (overlay->base)", async () => {
|
||||
const runSwapMixerInputsMutation = vi.fn(async () => undefined);
|
||||
const showConnectionRejectedToast = vi.fn();
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
helperResult={null}
|
||||
runSwapMixerInputsMutation={runSwapMixerInputsMutation}
|
||||
showConnectionRejectedToast={showConnectionRejectedToast}
|
||||
nodes={[
|
||||
{ id: "node-source-base", type: "image", position: { x: 0, y: 0 }, data: {} },
|
||||
{ id: "node-source-overlay", type: "asset", position: { x: 0, y: 120 }, data: {} },
|
||||
{ id: "node-mixer", type: "mixer", position: { x: 320, y: 120 }, data: {} },
|
||||
]}
|
||||
edges={[
|
||||
{
|
||||
id: "edge-base",
|
||||
source: "node-source-base",
|
||||
target: "node-mixer",
|
||||
targetHandle: "base",
|
||||
},
|
||||
{
|
||||
id: "edge-overlay",
|
||||
source: "node-source-overlay",
|
||||
target: "node-mixer",
|
||||
targetHandle: "overlay",
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const oldEdge = {
|
||||
id: "edge-overlay",
|
||||
source: "node-source-overlay",
|
||||
target: "node-mixer",
|
||||
targetHandle: "overlay",
|
||||
} as RFEdge;
|
||||
|
||||
await act(async () => {
|
||||
latestHandlersRef.current?.onReconnectStart();
|
||||
latestHandlersRef.current?.onReconnect(oldEdge, {
|
||||
source: "node-source-overlay",
|
||||
target: "node-mixer",
|
||||
sourceHandle: null,
|
||||
targetHandle: "base",
|
||||
});
|
||||
latestHandlersRef.current?.onReconnectEnd({} as MouseEvent, oldEdge);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(showConnectionRejectedToast).not.toHaveBeenCalled();
|
||||
expect(runSwapMixerInputsMutation).toHaveBeenCalledWith({
|
||||
canvasId: "canvas-1",
|
||||
edgeId: "edge-overlay",
|
||||
otherEdgeId: "edge-base",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not swap mixer reconnect when target mixer is not fully populated", async () => {
|
||||
const runCreateEdgeMutation = vi.fn(async () => undefined);
|
||||
const runSwapMixerInputsMutation = vi.fn(async () => undefined);
|
||||
const showConnectionRejectedToast = vi.fn();
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
helperResult={null}
|
||||
runCreateEdgeMutation={runCreateEdgeMutation}
|
||||
runSwapMixerInputsMutation={runSwapMixerInputsMutation}
|
||||
showConnectionRejectedToast={showConnectionRejectedToast}
|
||||
nodes={[
|
||||
{ id: "node-source-base", type: "image", position: { x: 0, y: 0 }, data: {} },
|
||||
{ id: "node-mixer", type: "mixer", position: { x: 320, y: 120 }, data: {} },
|
||||
]}
|
||||
edges={[
|
||||
{
|
||||
id: "edge-base",
|
||||
source: "node-source-base",
|
||||
target: "node-mixer",
|
||||
targetHandle: "base",
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const oldEdge = {
|
||||
id: "edge-base",
|
||||
source: "node-source-base",
|
||||
target: "node-mixer",
|
||||
targetHandle: "base",
|
||||
} as RFEdge;
|
||||
|
||||
await act(async () => {
|
||||
latestHandlersRef.current?.onReconnectStart();
|
||||
latestHandlersRef.current?.onReconnect(oldEdge, {
|
||||
source: "node-source-base",
|
||||
target: "node-mixer",
|
||||
sourceHandle: null,
|
||||
targetHandle: "overlay",
|
||||
});
|
||||
latestHandlersRef.current?.onReconnectEnd({} as MouseEvent, oldEdge);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(runSwapMixerInputsMutation).not.toHaveBeenCalled();
|
||||
expect(showConnectionRejectedToast).not.toHaveBeenCalled();
|
||||
expect(runCreateEdgeMutation).toHaveBeenCalledWith({
|
||||
canvasId: "canvas-1",
|
||||
sourceNodeId: "node-source-base",
|
||||
targetNodeId: "node-mixer",
|
||||
sourceHandle: undefined,
|
||||
targetHandle: "overlay",
|
||||
edgeIdToIgnore: "edge-base",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not perform mixer swap for non-mixer reconnect validation failures", async () => {
|
||||
const runSwapMixerInputsMutation = vi.fn(async () => undefined);
|
||||
const showConnectionRejectedToast = vi.fn();
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
helperResult={null}
|
||||
runSwapMixerInputsMutation={runSwapMixerInputsMutation}
|
||||
showConnectionRejectedToast={showConnectionRejectedToast}
|
||||
nodes={[
|
||||
{ id: "node-image", type: "image", position: { x: 0, y: 0 }, data: {} },
|
||||
{ id: "node-render", type: "render", position: { x: 300, y: 0 }, data: {} },
|
||||
]}
|
||||
edges={[
|
||||
{
|
||||
id: "edge-1",
|
||||
source: "node-image",
|
||||
target: "node-render",
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const oldEdge = {
|
||||
id: "edge-1",
|
||||
source: "node-image",
|
||||
target: "node-render",
|
||||
} as RFEdge;
|
||||
|
||||
await act(async () => {
|
||||
latestHandlersRef.current?.onReconnectStart();
|
||||
latestHandlersRef.current?.onReconnect(oldEdge, {
|
||||
source: "node-image",
|
||||
target: "node-image",
|
||||
sourceHandle: null,
|
||||
targetHandle: null,
|
||||
});
|
||||
});
|
||||
|
||||
expect(runSwapMixerInputsMutation).not.toHaveBeenCalled();
|
||||
expect(showConnectionRejectedToast).toHaveBeenCalledWith("self-loop");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user