import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { ConnectionDropMenu } from "@/components/ConnectionDropMenu"; describe("ConnectionDropMenu", () => { const mockOnSelect = vi.fn(); const mockOnClose = vi.fn(); const defaultProps = { position: { x: 100, y: 200 }, handleType: "image" as const, connectionType: "source" as const, onSelect: mockOnSelect, onClose: mockOnClose, }; beforeEach(() => { vi.clearAllMocks(); }); describe("Basic Rendering", () => { it("should render at the specified position", () => { render(); const menu = document.querySelector(".fixed"); expect(menu).toBeInTheDocument(); expect(menu).toHaveStyle({ left: "100px", top: "200px" }); }); it("should render the header with handle type", () => { render(); expect(screen.getByText("Add image node")).toBeInTheDocument(); }); it("should render keyboard shortcuts hint", () => { render(); expect(screen.getByText("navigate")).toBeInTheDocument(); expect(screen.getByText("select")).toBeInTheDocument(); }); it("should not render when handleType is null", () => { render(); expect(screen.queryByText(/Add .* node/)).not.toBeInTheDocument(); }); }); describe("Node Type Filtering - Source Connection (from output handle)", () => { it("should show image-accepting nodes when dragging from image output", () => { render(); expect(screen.getByText("Annotate")).toBeInTheDocument(); expect(screen.getByText("Generate Image")).toBeInTheDocument(); expect(screen.getByText("Generate Video")).toBeInTheDocument(); expect(screen.getByText("Split Grid Node")).toBeInTheDocument(); expect(screen.getByText("Split Grid Now")).toBeInTheDocument(); expect(screen.getByText("Output")).toBeInTheDocument(); }); it("should show text-accepting nodes when dragging from text output", () => { render(); expect(screen.getByText("Generate Image")).toBeInTheDocument(); expect(screen.getByText("Generate Video")).toBeInTheDocument(); expect(screen.getByText("LLM Generate")).toBeInTheDocument(); // Should NOT show image-only nodes expect(screen.queryByText("Annotate")).not.toBeInTheDocument(); expect(screen.queryByText("Output")).not.toBeInTheDocument(); }); it("should show video-accepting nodes when dragging from video output", () => { render(); expect(screen.getByText("Generate Video")).toBeInTheDocument(); expect(screen.getByText("Output")).toBeInTheDocument(); // Should NOT show image/text-only nodes expect(screen.queryByText("Annotate")).not.toBeInTheDocument(); expect(screen.queryByText("LLM Generate")).not.toBeInTheDocument(); }); it("should show 3D-accepting nodes when dragging from 3D output", () => { render(); expect(screen.getByText("3D Viewer")).toBeInTheDocument(); // Should NOT show image/text/video nodes expect(screen.queryByText("Annotate")).not.toBeInTheDocument(); expect(screen.queryByText("Generate Image")).not.toBeInTheDocument(); expect(screen.queryByText("Generate Video")).not.toBeInTheDocument(); }); }); describe("Node Type Filtering - Target Connection (from input handle)", () => { it("should show image-producing nodes when dragging from image input", () => { render(); expect(screen.getByText("Image Input")).toBeInTheDocument(); expect(screen.getByText("Annotate")).toBeInTheDocument(); expect(screen.getByText("Generate Image")).toBeInTheDocument(); // Should NOT show text nodes expect(screen.queryByText("Prompt")).not.toBeInTheDocument(); }); it("should show text-producing nodes when dragging from text input", () => { render(); expect(screen.getByText("Prompt")).toBeInTheDocument(); expect(screen.getByText("LLM Generate")).toBeInTheDocument(); // Should NOT show image-only nodes expect(screen.queryByText("Image Input")).not.toBeInTheDocument(); expect(screen.queryByText("Annotate")).not.toBeInTheDocument(); }); it("should show video-producing nodes when dragging from video input", () => { render(); expect(screen.getByText("Generate Video")).toBeInTheDocument(); // Should NOT show other nodes expect(screen.queryByText("Image Input")).not.toBeInTheDocument(); expect(screen.queryByText("Prompt")).not.toBeInTheDocument(); }); it("should show 3D-producing nodes when dragging from 3D input", () => { render(); expect(screen.getByText("Generate 3D")).toBeInTheDocument(); // Should NOT show other nodes expect(screen.queryByText("Image Input")).not.toBeInTheDocument(); expect(screen.queryByText("Generate Image")).not.toBeInTheDocument(); }); }); describe("Menu Item Click", () => { it("should call onSelect with node type when menu item is clicked", () => { render(); const annotateButton = screen.getByText("Annotate"); fireEvent.click(annotateButton); expect(mockOnSelect).toHaveBeenCalledWith({ type: "annotation", isAction: false }); }); it("should call onSelect with isAction true for action items", () => { render(); const splitNowButton = screen.getByText("Split Grid Now"); fireEvent.click(splitNowButton); expect(mockOnSelect).toHaveBeenCalledWith({ type: "splitGridImmediate", isAction: true }); }); it("should call onSelect with nanoBanana type for Generate Image", () => { render(); const generateButton = screen.getByText("Generate Image"); fireEvent.click(generateButton); expect(mockOnSelect).toHaveBeenCalledWith({ type: "nanoBanana", isAction: false }); }); }); describe("Escape Key", () => { it("should call onClose when Escape key is pressed", () => { render(); fireEvent.keyDown(document, { key: "Escape" }); expect(mockOnClose).toHaveBeenCalled(); }); }); describe("Click Outside", () => { it("should call onClose when clicking outside the menu", () => { const { container } = render(
Outside
); const outside = screen.getByTestId("outside"); fireEvent.mouseDown(outside); expect(mockOnClose).toHaveBeenCalled(); }); it("should not call onClose when clicking inside the menu", () => { render(); const menuItem = screen.getByText("Annotate"); fireEvent.mouseDown(menuItem); expect(mockOnClose).not.toHaveBeenCalled(); }); }); describe("Keyboard Navigation", () => { it("should navigate down with ArrowDown key", () => { render(); // First item should be highlighted by default const firstButton = screen.getByText("Annotate").closest("button"); expect(firstButton).toHaveClass("bg-neutral-700"); // Press ArrowDown fireEvent.keyDown(document, { key: "ArrowDown" }); // Second item should now be highlighted const secondButton = screen.getByText("Generate Image").closest("button"); expect(secondButton).toHaveClass("bg-neutral-700"); }); it("should navigate up with ArrowUp key", () => { render(); // Press ArrowUp to go to last item (wrapping) fireEvent.keyDown(document, { key: "ArrowUp" }); // Last item should now be highlighted const lastButton = screen.getByText("Image Compare").closest("button"); expect(lastButton).toHaveClass("bg-neutral-700"); }); it("should select item with Enter key", () => { render(); // Press Enter on first item fireEvent.keyDown(document, { key: "Enter" }); expect(mockOnSelect).toHaveBeenCalledWith({ type: "annotation", isAction: false }); }); it("should select navigated item with Enter key", () => { render(); // Navigate to second item fireEvent.keyDown(document, { key: "ArrowDown" }); // Press Enter fireEvent.keyDown(document, { key: "Enter" }); expect(mockOnSelect).toHaveBeenCalledWith({ type: "nanoBanana", isAction: false }); }); it("should wrap around when navigating past last item", () => { render(); // Text target options: Prompt, Prompt Constructor, nanoBanana, generateVideo, generateAudio, llmGenerate (6 items) // Navigate down 6 times to wrap to first fireEvent.keyDown(document, { key: "ArrowDown" }); fireEvent.keyDown(document, { key: "ArrowDown" }); fireEvent.keyDown(document, { key: "ArrowDown" }); fireEvent.keyDown(document, { key: "ArrowDown" }); fireEvent.keyDown(document, { key: "ArrowDown" }); fireEvent.keyDown(document, { key: "ArrowDown" }); // Should be back on first item (Prompt) const firstButton = screen.getByText("Prompt").closest("button"); expect(firstButton).toHaveClass("bg-neutral-700"); }); }); describe("Mouse Hover", () => { it("should highlight item on mouse enter", () => { render(); const secondItem = screen.getByText("Generate Image"); fireEvent.mouseEnter(secondItem); // Second item should now be highlighted const secondButton = secondItem.closest("button"); expect(secondButton).toHaveClass("bg-neutral-700"); }); }); });