Files
lemonspace_app/components/canvas/__tests__/canvas-delete-handlers.test.tsx

134 lines
3.8 KiB
TypeScript

// @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<typeof useCanvasDeleteHandlers> | null;
} = { current: null };
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
type HookHarnessProps = {
runBatchRemoveNodesMutation: ReturnType<typeof vi.fn>;
runCreateEdgeMutation: ReturnType<typeof vi.fn>;
};
function HookHarness({
runBatchRemoveNodesMutation,
runCreateEdgeMutation,
}: HookHarnessProps) {
const [nodes] = useState<RFNode[]>([
{ 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<RFEdge[]>([
{ 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<string>());
const [, setAssetBrowserTargetNodeId] = useState<string | null>(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(
<HookHarness
runBatchRemoveNodesMutation={runBatchRemoveNodesMutation}
runCreateEdgeMutation={runCreateEdgeMutation}
/>,
);
});
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");
});
});