import { beforeEach, describe, expect, it, vi } from 'vitest' import { computed, shallowRef } from 'vue' import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager' import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager' import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle' import type { LGraph, LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers' const canvasSelectedItems = vi.hoisted(() => [] as Array<{ id?: string }>) vi.mock('@/renderer/core/canvas/canvasStore', () => { const canvas: Partial = { select: vi.fn(), deselect: vi.fn(), deselectAll: vi.fn() } const updateSelectedItems = vi.fn() const canvasStoreInstance = { canvas: canvas as LGraphCanvas, updateSelectedItems, selectedItems: canvasSelectedItems } return { useCanvasStore: vi.fn(() => canvasStoreInstance) } }) vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({ useCanvasInteractions: vi.fn(() => ({ shouldHandleNodePointerEvents: computed(() => true) // Default to allowing pointer events })) })) vi.mock('@/renderer/core/layout/operations/layoutMutations', () => { const setSource = vi.fn() const bringNodeToFront = vi.fn() return { useLayoutMutations: vi.fn(() => ({ setSource, bringNodeToFront })) } }) vi.mock('@/composables/graph/useGraphNodeManager', () => { const mockNode = { id: 'node-1', selected: false, flags: { pinned: false } } const nodeManager = shallowRef({ getNode: vi.fn(() => mockNode as Partial as LGraphNode) } as Partial as GraphNodeManager) return { useGraphNodeManager: vi.fn(() => nodeManager) } }) vi.mock('@/composables/graph/useVueNodeLifecycle', () => { const nodeManager = useGraphNodeManager(undefined as unknown as LGraph) return { useVueNodeLifecycle: vi.fn(() => ({ nodeManager })) } }) describe('useNodeEventHandlers', () => { const { nodeManager: mockNodeManager } = useVueNodeLifecycle() const mockNode = mockNodeManager.value!.getNode('fake_id') const mockLayoutMutations = useLayoutMutations() const testNodeId = 'node-1' beforeEach(async () => { vi.resetAllMocks() canvasSelectedItems.length = 0 }) describe('handleNodeSelect', () => { it('should select single node on regular click', () => { const { handleNodeSelect } = useNodeEventHandlers() const { canvas, updateSelectedItems } = useCanvasStore() const event = new PointerEvent('pointerdown', { bubbles: true, ctrlKey: false, metaKey: false }) handleNodeSelect(event, testNodeId) expect(canvas?.deselectAll).toHaveBeenCalledOnce() expect(canvas?.select).toHaveBeenCalledWith(mockNode) expect(updateSelectedItems).toHaveBeenCalledOnce() }) it('on pointer down with ctrl+click: selects node immediately', () => { const { handleNodeSelect } = useNodeEventHandlers() const { canvas } = useCanvasStore() mockNode!.selected = false const ctrlClickEvent = new PointerEvent('pointerdown', { bubbles: true, ctrlKey: true, metaKey: false }) handleNodeSelect(ctrlClickEvent, testNodeId) // On pointer down with multi-select: bring to front expect(mockLayoutMutations.bringNodeToFront).toHaveBeenCalledWith( 'node-1' ) // Selection happens immediately so dragging includes this node expect(canvas?.deselectAll).not.toHaveBeenCalled() expect(canvas?.select).toHaveBeenCalledWith(mockNode) expect(canvas?.deselect).not.toHaveBeenCalled() }) it('on pointer down with ctrl+click of selected node: brings node to front only', () => { const { handleNodeSelect } = useNodeEventHandlers() const { canvas } = useCanvasStore() mockNode!.selected = true mockNode!.flags.pinned = false const ctrlClickEvent = new PointerEvent('pointerdown', { bubbles: true, ctrlKey: true, metaKey: false }) handleNodeSelect(ctrlClickEvent, testNodeId) // On pointer down: bring to front expect(mockLayoutMutations.bringNodeToFront).toHaveBeenCalledWith( 'node-1' ) // But don't deselect yet (deferred to pointer up) expect(canvas?.deselect).not.toHaveBeenCalled() expect(canvas?.select).not.toHaveBeenCalled() }) it('on pointer down with meta key (Cmd): selects node immediately', () => { const { handleNodeSelect } = useNodeEventHandlers() const { canvas } = useCanvasStore() mockNode!.selected = false mockNode!.flags.pinned = false const metaClickEvent = new PointerEvent('pointerdown', { bubbles: true, ctrlKey: false, metaKey: true }) handleNodeSelect(metaClickEvent, testNodeId) // On pointer down with meta key: bring to front expect(mockLayoutMutations.bringNodeToFront).toHaveBeenCalledWith( 'node-1' ) // Selection happens immediately expect(canvas?.select).toHaveBeenCalledWith(mockNode) expect(canvas?.deselectAll).not.toHaveBeenCalled() expect(canvas?.deselect).not.toHaveBeenCalled() }) it('on pointer down with shift key: selects node immediately', () => { const { handleNodeSelect } = useNodeEventHandlers() const { canvas } = useCanvasStore() mockNode!.selected = false mockNode!.flags.pinned = false const shiftClickEvent = new PointerEvent('pointerdown', { bubbles: true, shiftKey: true }) handleNodeSelect(shiftClickEvent, testNodeId) // On pointer down with shift: bring to front expect(mockLayoutMutations.bringNodeToFront).toHaveBeenCalledWith( 'node-1' ) // Selection happens immediately for shift-click as well expect(canvas?.select).toHaveBeenCalledWith(mockNode) expect(canvas?.deselectAll).not.toHaveBeenCalled() expect(canvas?.deselect).not.toHaveBeenCalled() }) it('keeps existing multi-selection when dragging selected node without modifiers', () => { const { handleNodeSelect } = useNodeEventHandlers() const { canvas } = useCanvasStore() mockNode!.selected = true canvasSelectedItems.push({ id: 'node-1' }, { id: 'node-2' }) const event = new PointerEvent('pointerdown', { bubbles: true, ctrlKey: false, metaKey: false }) handleNodeSelect(event, testNodeId) expect(canvas?.deselectAll).not.toHaveBeenCalled() expect(canvas?.select).not.toHaveBeenCalled() }) it('should bring node to front when not pinned', () => { const { handleNodeSelect } = useNodeEventHandlers() mockNode!.flags.pinned = false const event = new PointerEvent('pointerdown') handleNodeSelect(event, testNodeId) expect(mockLayoutMutations.bringNodeToFront).toHaveBeenCalledWith( 'node-1' ) }) it('should not bring pinned node to front', () => { const { handleNodeSelect } = useNodeEventHandlers() mockNode!.flags.pinned = true const event = new PointerEvent('pointerdown') handleNodeSelect(event, testNodeId) expect(mockLayoutMutations.bringNodeToFront).not.toHaveBeenCalled() }) }) describe('toggleNodeSelectionAfterPointerUp', () => { it('on pointer up with multi-select: deselects node that was selected at pointer down', () => { const { toggleNodeSelectionAfterPointerUp } = useNodeEventHandlers() const { canvas, updateSelectedItems } = useCanvasStore() mockNode!.selected = true toggleNodeSelectionAfterPointerUp('node-1', true) expect(canvas?.deselect).toHaveBeenCalledWith(mockNode) expect(updateSelectedItems).toHaveBeenCalledOnce() }) it('on pointer up with multi-select and node not previously selected: no-op', () => { const { toggleNodeSelectionAfterPointerUp } = useNodeEventHandlers() const { canvas, updateSelectedItems } = useCanvasStore() mockNode!.selected = true toggleNodeSelectionAfterPointerUp('node-1', true) expect(canvas?.select).not.toHaveBeenCalled() expect(updateSelectedItems).toHaveBeenCalled() }) it('on pointer up without multi-select: collapses multi-selection to clicked node', () => { const { toggleNodeSelectionAfterPointerUp } = useNodeEventHandlers() const { canvas, updateSelectedItems } = useCanvasStore() mockNode!.selected = true canvasSelectedItems.push({ id: 'node-1' }, { id: 'node-2' }) toggleNodeSelectionAfterPointerUp('node-1', false) expect(canvas?.deselectAll).toHaveBeenCalledOnce() expect(canvas?.select).toHaveBeenCalledWith(mockNode) expect(updateSelectedItems).toHaveBeenCalledOnce() }) it('on pointer up without multi-select: keeps single selection intact', () => { const { toggleNodeSelectionAfterPointerUp } = useNodeEventHandlers() const { canvas, updateSelectedItems } = useCanvasStore() mockNode!.selected = true canvasSelectedItems.push({ id: 'node-1' }) toggleNodeSelectionAfterPointerUp('node-1', false) expect(canvas?.select).toHaveBeenCalledWith(mockNode) expect(updateSelectedItems).toHaveBeenCalled() }) }) })