// @vitest-environment jsdom import { act, useEffect, useRef, useState } from "react"; import { createRoot, type Root } from "react-dom/client"; import type { Edge as RFEdge, Node as RFNode } from "@xyflow/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { Id } from "@/convex/_generated/dataModel"; const mocks = vi.hoisted(() => ({ enqueueCanvasSyncOp: vi.fn(async () => ({ replacedIds: [] as string[] })), countCanvasSyncOps: vi.fn(async () => 0), listCanvasSyncOps: vi.fn(async () => []), mutationMocks: new Map>(), })); vi.mock("@/convex/_generated/api", () => ({ api: { nodes: { move: "nodes.move", resize: "nodes.resize", updateData: "nodes.updateData", create: "nodes.create", createWithEdgeFromSource: "nodes.createWithEdgeFromSource", createWithEdgeToTarget: "nodes.createWithEdgeToTarget", createWithEdgeSplit: "nodes.createWithEdgeSplit", batchRemove: "nodes.batchRemove", splitEdgeAtExistingNode: "nodes.splitEdgeAtExistingNode", }, edges: { create: "edges.create", remove: "edges.remove", }, }, })); vi.mock("convex/react", () => ({ useConvexConnectionState: () => ({ isWebSocketConnected: true }), useMutation: (key: unknown) => { let mutation = mocks.mutationMocks.get(key); if (!mutation) { mutation = vi.fn(async () => undefined); Object.assign(mutation, { withOptimisticUpdate: () => mutation, }); mocks.mutationMocks.set(key, mutation); } return mutation; }, })); vi.mock("@/lib/canvas-op-queue", () => ({ ackCanvasSyncOp: vi.fn(async () => undefined), countCanvasSyncOps: mocks.countCanvasSyncOps, dropCanvasSyncOpsByClientRequestIds: vi.fn(async () => []), dropCanvasSyncOpsByEdgeIds: vi.fn(async () => []), dropCanvasSyncOpsByNodeIds: vi.fn(async () => []), dropExpiredCanvasSyncOps: vi.fn(async () => []), enqueueCanvasSyncOp: mocks.enqueueCanvasSyncOp, listCanvasSyncOps: mocks.listCanvasSyncOps, markCanvasSyncOpFailed: vi.fn(async () => undefined), remapCanvasSyncNodeId: vi.fn(async () => 0), })); vi.mock("@/lib/canvas-local-persistence", () => ({ dropCanvasOpsByClientRequestIds: vi.fn(() => []), dropCanvasOpsByEdgeIds: vi.fn(() => []), dropCanvasOpsByNodeIds: vi.fn(() => []), enqueueCanvasOp: vi.fn(() => "op-1"), remapCanvasOpNodeId: vi.fn(() => 0), resolveCanvasOp: vi.fn(() => undefined), resolveCanvasOps: vi.fn(() => undefined), })); vi.mock("@/lib/toast", () => ({ toast: { info: vi.fn(), warning: vi.fn(), }, })); import { useCanvasSyncEngine } from "@/components/canvas/use-canvas-sync-engine"; const asCanvasId = (id: string): Id<"canvases"> => id as Id<"canvases">; const asNodeId = (id: string): Id<"nodes"> => id as Id<"nodes">; const latestHookValueRef: { current: ReturnType | null; } = { current: null }; (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; function HookHarness({ canvasId }: { canvasId: Id<"canvases"> }) { const [, setNodes] = useState([]); const [edges, setEdges] = useState([]); const edgesRef = useRef(edges); const deletingNodeIds = useRef(new Set()); const [, setAssetBrowserTargetNodeId] = useState(null); const [, setEdgeSyncNonce] = useState(0); useEffect(() => { edgesRef.current = edges; }, [edges]); const hookValue = useCanvasSyncEngine({ canvasId, setNodes, setEdges, edgesRef, setAssetBrowserTargetNodeId, setEdgeSyncNonce, deletingNodeIds, }); useEffect(() => { latestHookValueRef.current = hookValue; }, [hookValue]); return null; } describe("useCanvasSyncEngine hook wiring", () => { let container: HTMLDivElement | null = null; let root: Root | null = null; afterEach(async () => { latestHookValueRef.current = null; mocks.mutationMocks.clear(); vi.clearAllMocks(); if (root) { await act(async () => { root?.unmount(); }); } container?.remove(); root = null; container = null; }); it("uses the latest canvas id after rerendering the mounted hook", async () => { container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); await act(async () => { root?.render(); }); await act(async () => { root?.render(); }); await act(async () => { await latestHookValueRef.current?.actions.resizeNode({ nodeId: asNodeId("node-1"), width: 480, height: 320, }); }); expect(mocks.enqueueCanvasSyncOp).toHaveBeenLastCalledWith( expect.objectContaining({ canvasId: "canvas-2", type: "resizeNode", payload: { nodeId: "node-1", width: 480, height: 320, }, }), ); }); });