diff --git a/components/canvas/__tests__/use-canvas-connections.test.tsx b/components/canvas/__tests__/use-canvas-connections.test.tsx deleted file mode 100644 index 6a2a69c..0000000 --- a/components/canvas/__tests__/use-canvas-connections.test.tsx +++ /dev/null @@ -1,322 +0,0 @@ -// @vitest-environment jsdom - -import React, { act, useEffect, useRef } from "react"; -import { createRoot, type Root } from "react-dom/client"; -import type { Connection, Edge as RFEdge, Node as RFNode } from "@xyflow/react"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import type { Id } from "@/convex/_generated/dataModel"; -import type { CanvasNodeTemplate } from "@/lib/canvas-node-templates"; -import { CANVAS_NODE_TEMPLATES } from "@/lib/canvas-node-templates"; -import { useCanvasConnections } from "@/components/canvas/use-canvas-connections"; - -const { - validateCanvasConnectionMock, - validateCanvasConnectionByTypeMock, - useCanvasReconnectHandlersMock, - getConnectEndClientPointMock, -} = vi.hoisted(() => ({ - validateCanvasConnectionMock: vi.fn(), - validateCanvasConnectionByTypeMock: vi.fn(), - useCanvasReconnectHandlersMock: vi.fn(), - getConnectEndClientPointMock: vi.fn(), -})); - -vi.mock("@/components/canvas/canvas-connection-validation", () => ({ - validateCanvasConnection: validateCanvasConnectionMock, - validateCanvasConnectionByType: validateCanvasConnectionByTypeMock, -})); - -vi.mock("@/components/canvas/canvas-reconnect", () => ({ - useCanvasReconnectHandlers: useCanvasReconnectHandlersMock, -})); - -vi.mock("@/components/canvas/canvas-helpers", async () => { - const actual = await vi.importActual< - typeof import("@/components/canvas/canvas-helpers") - >("@/components/canvas/canvas-helpers"); - - return { - ...actual, - getConnectEndClientPoint: getConnectEndClientPointMock, - }; -}); - -const asCanvasId = (id: string): Id<"canvases"> => id as Id<"canvases">; -const asNodeId = (id: string): Id<"nodes"> => id as Id<"nodes">; - -type HarnessProps = { - nodes: RFNode[]; - edges: RFEdge[]; - runCreateEdgeMutation?: ReturnType; - runRemoveEdgeMutation?: ReturnType; - runCreateNodeWithEdgeFromSourceOnlineOnly?: ReturnType; - runCreateNodeWithEdgeToTargetOnlineOnly?: ReturnType; - syncPendingMoveForClientRequest?: ReturnType; - showConnectionRejectedToast?: ReturnType; - isReconnectDragActive?: boolean; -}; - -const latestHookRef: { - current: ReturnType | null; -} = { current: null }; - -(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; - -function HookHarness(props: HarnessProps) { - const nodesRef = useRef(props.nodes); - const edgesRef = useRef(props.edges); - const pendingConnectionCreatesRef = useRef(new Set()); - const resolvedRealIdByClientRequestRef = useRef(new Map>()); - const edgeReconnectSuccessful = useRef(true); - const isReconnectDragActiveRef = useRef(Boolean(props.isReconnectDragActive)); - - useEffect(() => { - nodesRef.current = props.nodes; - edgesRef.current = props.edges; - }, [props.edges, props.nodes]); - - const hookValue = useCanvasConnections({ - canvasId: asCanvasId("canvas-1"), - nodes: props.nodes, - edges: props.edges, - nodesRef, - edgesRef, - edgeReconnectSuccessful, - isReconnectDragActiveRef, - pendingConnectionCreatesRef, - resolvedRealIdByClientRequestRef, - setEdges: vi.fn(), - setEdgeSyncNonce: vi.fn(), - screenToFlowPosition: ({ x, y }: { x: number; y: number }) => ({ x: x - 10, y: y - 20 }), - syncPendingMoveForClientRequest: - props.syncPendingMoveForClientRequest ?? vi.fn(async () => undefined), - runCreateEdgeMutation: props.runCreateEdgeMutation ?? vi.fn(async () => undefined), - runRemoveEdgeMutation: props.runRemoveEdgeMutation ?? vi.fn(async () => undefined), - runCreateNodeWithEdgeFromSourceOnlineOnly: - props.runCreateNodeWithEdgeFromSourceOnlineOnly ?? vi.fn(async () => asNodeId("node-new")), - runCreateNodeWithEdgeToTargetOnlineOnly: - props.runCreateNodeWithEdgeToTargetOnlineOnly ?? vi.fn(async () => asNodeId("node-new")), - showConnectionRejectedToast: - props.showConnectionRejectedToast ?? vi.fn(), - }); - - useEffect(() => { - latestHookRef.current = hookValue; - }, [hookValue]); - - return null; -} - -describe("useCanvasConnections", () => { - let container: HTMLDivElement | null = null; - let root: Root | null = null; - - beforeEach(() => { - validateCanvasConnectionMock.mockReturnValue(null); - validateCanvasConnectionByTypeMock.mockReturnValue(null); - getConnectEndClientPointMock.mockReturnValue({ x: 140, y: 220 }); - useCanvasReconnectHandlersMock.mockReturnValue({ - onReconnectStart: vi.fn(), - onReconnect: vi.fn(), - onReconnectEnd: vi.fn(), - }); - }); - - afterEach(async () => { - latestHookRef.current = null; - vi.clearAllMocks(); - if (root) { - await act(async () => { - root?.unmount(); - }); - } - container?.remove(); - root = null; - container = null; - }); - - async function renderHook(props: HarnessProps) { - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - - await act(async () => { - root?.render(); - }); - } - - it("creates a valid edge through centralized validation", async () => { - const runCreateEdgeMutation = vi.fn(async () => undefined); - - await renderHook({ - nodes: [ - { id: "node-image", type: "image", position: { x: 0, y: 0 }, data: {} }, - { id: "node-text", type: "text", position: { x: 10, y: 10 }, data: {} }, - ], - edges: [], - runCreateEdgeMutation, - }); - - const connection: Connection = { - source: "node-image", - target: "node-text", - sourceHandle: null, - targetHandle: null, - }; - - await act(async () => { - latestHookRef.current?.onConnect(connection); - }); - - expect(validateCanvasConnectionMock).toHaveBeenCalledWith(connection, expect.any(Array), []); - expect(runCreateEdgeMutation).toHaveBeenCalledWith({ - canvasId: "canvas-1", - sourceNodeId: "node-image", - targetNodeId: "node-text", - sourceHandle: undefined, - targetHandle: undefined, - }); - }); - - it("rejects invalid connections without mutating edges", async () => { - const runCreateEdgeMutation = vi.fn(async () => undefined); - const showConnectionRejectedToast = vi.fn(); - validateCanvasConnectionMock.mockReturnValue("self-loop"); - - await renderHook({ - nodes: [ - { id: "node-image", type: "image", position: { x: 0, y: 0 }, data: {} }, - ], - edges: [], - runCreateEdgeMutation, - showConnectionRejectedToast, - }); - - await act(async () => { - latestHookRef.current?.onConnect({ - source: "node-image", - target: "node-image", - sourceHandle: null, - targetHandle: null, - }); - }); - - expect(showConnectionRejectedToast).toHaveBeenCalledWith("self-loop"); - expect(runCreateEdgeMutation).not.toHaveBeenCalled(); - }); - - it("opens the connection drop menu from an invalid connect end", async () => { - await renderHook({ - nodes: [ - { id: "node-image", type: "image", position: { x: 0, y: 0 }, data: {} }, - ], - edges: [], - }); - - await act(async () => { - latestHookRef.current?.onConnectEnd({} as MouseEvent, { - isValid: false, - fromNode: { id: "node-image" }, - fromHandle: { id: "source", type: "source" }, - } as never); - }); - - expect(latestHookRef.current?.connectionDropMenu).toEqual({ - screenX: 140, - screenY: 220, - flowX: 130, - flowY: 200, - fromNodeId: "node-image", - fromHandleId: "source", - fromHandleType: "source", - }); - }); - - it("routes connection-drop creation through type validation and source creation", async () => { - const runCreateNodeWithEdgeFromSourceOnlineOnly = vi.fn(async () => asNodeId("node-new")); - const syncPendingMoveForClientRequest = vi.fn(async () => undefined); - - await renderHook({ - nodes: [ - { id: "node-image", type: "image", position: { x: 0, y: 0 }, data: {} }, - ], - edges: [], - runCreateNodeWithEdgeFromSourceOnlineOnly, - syncPendingMoveForClientRequest, - }); - - await act(async () => { - latestHookRef.current?.onConnectEnd({} as MouseEvent, { - isValid: false, - fromNode: { id: "node-image" }, - fromHandle: { id: "source", type: "source" }, - } as never); - }); - - const template = { - ...CANVAS_NODE_TEMPLATES.find((entry) => entry.type === "text")!, - defaultData: { content: "Hello" }, - } as unknown as CanvasNodeTemplate; - - await act(async () => { - latestHookRef.current?.handleConnectionDropPick(template); - await Promise.resolve(); - }); - - expect(validateCanvasConnectionByTypeMock).toHaveBeenCalledWith({ - sourceType: "image", - targetType: "text", - targetNodeId: expect.stringMatching(/^__pending_text_/), - edges: [], - }); - expect(runCreateNodeWithEdgeFromSourceOnlineOnly).toHaveBeenCalledWith( - expect.objectContaining({ - canvasId: "canvas-1", - type: "text", - positionX: 130, - positionY: 200, - sourceNodeId: "node-image", - sourceHandle: "source", - data: expect.objectContaining({ - canvasId: "canvas-1", - content: "Hello", - }), - }), - ); - expect(syncPendingMoveForClientRequest).toHaveBeenCalled(); - }); - - it("adapts reconnect handlers through the shared connection validation", async () => { - const showConnectionRejectedToast = vi.fn(); - - await renderHook({ - nodes: [ - { id: "node-image", type: "image", position: { x: 0, y: 0 }, data: {} }, - { id: "node-text", type: "text", position: { x: 10, y: 10 }, data: {} }, - ], - edges: [{ id: "edge-1", source: "node-image", target: "node-text" }], - showConnectionRejectedToast, - }); - - const reconnectArgs = useCanvasReconnectHandlersMock.mock.calls[0][0]; - const reconnectValidation = reconnectArgs.validateConnection( - { id: "edge-1", source: "node-image", target: "node-text" }, - { source: "node-image", target: "node-text" }, - ); - - reconnectArgs.onInvalidConnection("unknown-node"); - - expect(validateCanvasConnectionMock).toHaveBeenCalledWith( - { source: "node-image", target: "node-text" }, - expect.any(Array), - expect.any(Array), - "edge-1", - ); - expect(reconnectValidation).toBeNull(); - expect(showConnectionRejectedToast).toHaveBeenCalledWith("unknown-node"); - expect(latestHookRef.current?.onReconnect).toBe( - useCanvasReconnectHandlersMock.mock.results[0]?.value.onReconnect, - ); - }); -}); diff --git a/vitest.config.ts b/vitest.config.ts index 62aaffa..3600b04 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,7 +12,6 @@ export default defineConfig({ include: [ "tests/**/*.test.ts", "components/canvas/__tests__/canvas-flow-reconciliation-helpers.test.ts", - "components/canvas/__tests__/use-canvas-connections.test.tsx", "components/canvas/__tests__/use-canvas-flow-reconciliation.test.ts", "components/canvas/__tests__/use-canvas-node-interactions.test.tsx", "components/canvas/__tests__/use-canvas-sync-engine.test.ts",