// @vitest-environment jsdom import React, { 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 toastInfoMock = vi.hoisted(() => vi.fn()); vi.mock("@/lib/toast", () => ({ toast: { warning: vi.fn(), info: toastInfoMock, }, })); import { useCanvasDeleteHandlers } from "@/components/canvas/canvas-delete-handlers"; const asCanvasId = (id: string): Id<"canvases"> => id as Id<"canvases">; const latestHandlersRef: { current: ReturnType | null; } = { current: null }; (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; type HookHarnessProps = { runBatchRemoveNodesMutation: ReturnType; runCreateEdgeMutation: ReturnType; }; function HookHarness({ runBatchRemoveNodesMutation, runCreateEdgeMutation, }: HookHarnessProps) { const [nodes] = useState([ { id: "node-source", type: "image", position: { x: 0, y: 0 }, data: {} }, { id: "node-middle", type: "note", position: { x: 120, y: 0 }, data: {} }, { id: "node-target", type: "text", position: { x: 240, y: 0 }, data: {} }, ]); const [edges] = useState([ { id: "edge-source-middle", source: "node-source", target: "node-middle" }, { id: "edge-middle-target", source: "node-middle", target: "node-target" }, ]); const nodesRef = useRef(nodes); const edgesRef = useRef(edges); const deletingNodeIds = useRef(new Set()); const [, setAssetBrowserTargetNodeId] = useState(null); const handlers = useCanvasDeleteHandlers({ t: ((key: string) => key) as never, canvasId: asCanvasId("canvas-1"), nodes, edges, nodesRef, edgesRef, deletingNodeIds, setAssetBrowserTargetNodeId, runBatchRemoveNodesMutation, runCreateEdgeMutation, runRemoveEdgeMutation: vi.fn(async () => undefined), }); useEffect(() => { latestHandlersRef.current = handlers; }, [handlers]); return null; } describe("useCanvasDeleteHandlers", () => { let container: HTMLDivElement | null = null; let root: Root | null = null; afterEach(async () => { latestHandlersRef.current = null; vi.useRealTimers(); vi.clearAllMocks(); if (root) { await act(async () => { root?.unmount(); }); } container?.remove(); root = null; container = null; }); it("retries bridge edge creation when the first create fails", async () => { vi.useFakeTimers(); const runBatchRemoveNodesMutation = vi.fn(async () => undefined); const runCreateEdgeMutation = vi .fn(async () => undefined) .mockRejectedValueOnce(new Error("incoming limit reached")); container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); await act(async () => { root?.render( , ); }); await act(async () => { latestHandlersRef.current?.onNodesDelete([ { id: "node-middle", type: "note", position: { x: 120, y: 0 }, data: {}, }, ]); await Promise.resolve(); }); await act(async () => { vi.runAllTimers(); await Promise.resolve(); await Promise.resolve(); }); expect(runBatchRemoveNodesMutation).toHaveBeenCalledTimes(1); expect(runCreateEdgeMutation).toHaveBeenCalledTimes(2); expect(toastInfoMock).toHaveBeenCalledWith("canvas.nodesRemoved"); }); });