From 109cd3fb5b89824d52699ad6f31181af6789b5e1 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Tue, 13 Jan 2026 12:49:51 +1300 Subject: [PATCH] test(17-07): add ConnectionDropMenu component tests - Add tests for basic rendering (position, header, shortcuts) - Add tests for node type filtering by handle type (source/target) - Add tests for menu item click behavior - Add tests for keyboard navigation (arrows, enter, escape) - Add tests for click outside closing - Add tests for mouse hover highlighting Co-Authored-By: Claude Opus 4.5 --- .../__tests__/ConnectionDropMenu.test.tsx | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 src/components/__tests__/ConnectionDropMenu.test.tsx diff --git a/src/components/__tests__/ConnectionDropMenu.test.tsx b/src/components/__tests__/ConnectionDropMenu.test.tsx new file mode 100644 index 00000000..c5851137 --- /dev/null +++ b/src/components/__tests__/ConnectionDropMenu.test.tsx @@ -0,0 +1,253 @@ +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(); + }); + }); + + 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(); + }); + }); + + 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("Output").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 options: nanoBanana, generateVideo, llmGenerate (3 items) + // Navigate down 3 times to wrap to first + fireEvent.keyDown(document, { key: "ArrowDown" }); + fireEvent.keyDown(document, { key: "ArrowDown" }); + fireEvent.keyDown(document, { key: "ArrowDown" }); + + // Should be back on first item + const firstButton = screen.getByText("Generate Image").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"); + }); + }); +});