Files
ComfyUI_frontend/src/composables/node/useNodeDragToCanvas.test.ts
pythongosssss b3ba6c9344 fix: select node after adding from library (#12404)
## Summary

When adding a node from the library sidebar, the node was not correctly
selected upon placing it. This was due to the canvas capturing the node
under the cursor on mouse down, however the node had not yet been
comitted to the graph at that point, and so selection was then cleared
on mouse up.

## Changes

- **What**: 
- add `blockCommitPointerDown` so if the cursor is over the canvas stop
propagation to prevent LiteGraph adding the mouse handler to clear the
selection

## Review Focus

Alternative approaches considered were blocking the event in endDrag
however this then required manual cleanup of LiteGraph handlers or
overriding the `pointer.onClick` function to force selection of our
node, both felt worse than this approach.

## Screenshots (if applicable)


https://github.com/user-attachments/assets/a2eb154e-5178-4a1e-b5c7-884efd7a10c6
2026-05-21 19:52:56 +00:00

460 lines
12 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import type { useNodeDragToCanvas as UseNodeDragToCanvasType } from './useNodeDragToCanvas'
const {
mockAddNodeOnGraph,
mockConvertEventToCanvasOffset,
mockSelectItems,
mockCanvas
} = vi.hoisted(() => {
const mockConvertEventToCanvasOffset = vi.fn()
const mockSelectItems = vi.fn()
return {
mockAddNodeOnGraph: vi.fn(),
mockConvertEventToCanvasOffset,
mockSelectItems,
mockCanvas: {
canvas: {
getBoundingClientRect: vi.fn()
},
convertEventToCanvasOffset: mockConvertEventToCanvasOffset,
selectItems: mockSelectItems
}
}
})
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: vi.fn(() => ({
canvas: mockCanvas
}))
}))
vi.mock('@/services/litegraphService', () => ({
useLitegraphService: vi.fn(() => ({
addNodeOnGraph: mockAddNodeOnGraph
}))
}))
describe('useNodeDragToCanvas', () => {
let useNodeDragToCanvas: typeof UseNodeDragToCanvasType
const mockNodeDef = {
name: 'TestNode',
display_name: 'Test Node'
} as ComfyNodeDefImpl
beforeEach(async () => {
vi.resetModules()
vi.resetAllMocks()
const module = await import('./useNodeDragToCanvas')
useNodeDragToCanvas = module.useNodeDragToCanvas
})
afterEach(() => {
const { cleanupGlobalListeners } = useNodeDragToCanvas()
cleanupGlobalListeners()
vi.restoreAllMocks()
})
describe('startDrag', () => {
it('should set isDragging to true and store the node definition', () => {
const { isDragging, draggedNode, startDrag } = useNodeDragToCanvas()
expect(isDragging.value).toBe(false)
expect(draggedNode.value).toBeNull()
startDrag(mockNodeDef)
expect(isDragging.value).toBe(true)
expect(draggedNode.value).toBe(mockNodeDef)
})
it('should set dragMode to click by default', () => {
const { dragMode, startDrag } = useNodeDragToCanvas()
startDrag(mockNodeDef)
expect(dragMode.value).toBe('click')
})
it('should set dragMode to native when specified', () => {
const { dragMode, startDrag } = useNodeDragToCanvas()
startDrag(mockNodeDef, 'native')
expect(dragMode.value).toBe('native')
})
})
describe('cancelDrag', () => {
it('should reset isDragging and draggedNode', () => {
const { isDragging, draggedNode, startDrag, cancelDrag } =
useNodeDragToCanvas()
startDrag(mockNodeDef)
expect(isDragging.value).toBe(true)
cancelDrag()
expect(isDragging.value).toBe(false)
expect(draggedNode.value).toBeNull()
})
it('should reset dragMode to click', () => {
const { dragMode, startDrag, cancelDrag } = useNodeDragToCanvas()
startDrag(mockNodeDef, 'native')
expect(dragMode.value).toBe('native')
cancelDrag()
expect(dragMode.value).toBe('click')
})
})
describe('setupGlobalListeners', () => {
it('should add event listeners to document', () => {
const addEventListenerSpy = vi.spyOn(document, 'addEventListener')
const { setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
expect(addEventListenerSpy).toHaveBeenCalledWith(
'pointermove',
expect.any(Function)
)
expect(addEventListenerSpy).toHaveBeenCalledWith(
'pointerdown',
expect.any(Function),
true
)
expect(addEventListenerSpy).toHaveBeenCalledWith(
'pointerup',
expect.any(Function),
true
)
expect(addEventListenerSpy).toHaveBeenCalledWith(
'keydown',
expect.any(Function)
)
})
it('should only setup listeners once', () => {
const addEventListenerSpy = vi.spyOn(document, 'addEventListener')
const { setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
const callCount = addEventListenerSpy.mock.calls.length
setupGlobalListeners()
expect(addEventListenerSpy.mock.calls.length).toBe(callCount)
})
})
describe('cursorPosition', () => {
it('should update on pointermove', () => {
const { cursorPosition, setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
const pointerEvent = new PointerEvent('pointermove', {
clientX: 100,
clientY: 200
})
document.dispatchEvent(pointerEvent)
expect(cursorPosition.value).toEqual({ x: 100, y: 200 })
})
})
describe('endDrag behavior', () => {
it('should add node when pointer is over canvas', () => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
const pointerEvent = new PointerEvent('pointerup', {
clientX: 250,
clientY: 250,
bubbles: true
})
document.dispatchEvent(pointerEvent)
expect(mockAddNodeOnGraph).toHaveBeenCalledWith(mockNodeDef, {
pos: [150, 150]
})
})
it('should not add node when pointer is outside canvas', () => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
const { startDrag, setupGlobalListeners, isDragging } =
useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
const pointerEvent = new PointerEvent('pointerup', {
clientX: 600,
clientY: 250,
bubbles: true
})
document.dispatchEvent(pointerEvent)
expect(mockAddNodeOnGraph).not.toHaveBeenCalled()
expect(isDragging.value).toBe(false)
})
it('should cancel drag on Escape key', () => {
const { startDrag, setupGlobalListeners, isDragging } =
useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
expect(isDragging.value).toBe(true)
const keyEvent = new KeyboardEvent('keydown', { key: 'Escape' })
document.dispatchEvent(keyEvent)
expect(isDragging.value).toBe(false)
})
it('should not cancel drag on other keys', () => {
const { startDrag, setupGlobalListeners, isDragging } =
useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
const keyEvent = new KeyboardEvent('keydown', { key: 'Enter' })
document.dispatchEvent(keyEvent)
expect(isDragging.value).toBe(true)
})
it('should select the placed node when one is returned from the graph', () => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
const placedNode = { id: 1 }
mockAddNodeOnGraph.mockReturnValue(placedNode)
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
document.dispatchEvent(
new PointerEvent('pointerup', {
clientX: 250,
clientY: 250,
bubbles: true
})
)
expect(mockSelectItems).toHaveBeenCalledWith([placedNode])
})
it('should not call selectItems when graph returns no node', () => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
mockAddNodeOnGraph.mockReturnValue(null)
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
document.dispatchEvent(
new PointerEvent('pointerup', {
clientX: 250,
clientY: 250,
bubbles: true
})
)
expect(mockSelectItems).not.toHaveBeenCalled()
})
it('should not add node on pointerup when in native drag mode', () => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
const { startDrag, setupGlobalListeners, isDragging } =
useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef, 'native')
const pointerEvent = new PointerEvent('pointerup', {
clientX: 250,
clientY: 250,
bubbles: true
})
document.dispatchEvent(pointerEvent)
expect(mockAddNodeOnGraph).not.toHaveBeenCalled()
expect(isDragging.value).toBe(true)
})
})
describe('handleNativeDrop', () => {
it('should add node when drop position is over canvas', () => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
mockConvertEventToCanvasOffset.mockReturnValue([200, 200])
const { startDrag, handleNativeDrop } = useNodeDragToCanvas()
startDrag(mockNodeDef, 'native')
handleNativeDrop(250, 250)
expect(mockAddNodeOnGraph).toHaveBeenCalledWith(mockNodeDef, {
pos: [200, 200]
})
})
it('should not add node when drop position is outside canvas', () => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
const { startDrag, handleNativeDrop, isDragging } = useNodeDragToCanvas()
startDrag(mockNodeDef, 'native')
handleNativeDrop(600, 250)
expect(mockAddNodeOnGraph).not.toHaveBeenCalled()
expect(isDragging.value).toBe(false)
})
it('should not add node when dragMode is click', () => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
mockConvertEventToCanvasOffset.mockReturnValue([200, 200])
const { startDrag, handleNativeDrop } = useNodeDragToCanvas()
startDrag(mockNodeDef, 'click')
handleNativeDrop(250, 250)
expect(mockAddNodeOnGraph).not.toHaveBeenCalled()
})
it('should reset drag state after drop', () => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
mockConvertEventToCanvasOffset.mockReturnValue([200, 200])
const { startDrag, handleNativeDrop, isDragging, dragMode } =
useNodeDragToCanvas()
startDrag(mockNodeDef, 'native')
handleNativeDrop(250, 250)
expect(isDragging.value).toBe(false)
expect(dragMode.value).toBe('click')
})
})
describe('blockCommitPointerDown', () => {
function dispatchPointerDown(x: number, y: number) {
const event = new PointerEvent('pointerdown', {
clientX: x,
clientY: y,
bubbles: true,
cancelable: true
})
const stopSpy = vi.spyOn(event, 'stopImmediatePropagation')
document.dispatchEvent(event)
return stopSpy
}
beforeEach(() => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
})
it('should stop propagation when in click-drag mode over canvas', () => {
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
expect(dispatchPointerDown(250, 250)).toHaveBeenCalled()
})
it('should not stop propagation when not dragging', () => {
const { setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
expect(dispatchPointerDown(250, 250)).not.toHaveBeenCalled()
})
it('should not stop propagation in native drag mode', () => {
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef, 'native')
expect(dispatchPointerDown(250, 250)).not.toHaveBeenCalled()
})
it('should not stop propagation when pointer is outside canvas', () => {
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
expect(dispatchPointerDown(600, 250)).not.toHaveBeenCalled()
})
})
})