mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 21:38:52 +00:00
## 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
460 lines
12 KiB
TypeScript
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()
|
|
})
|
|
})
|
|
})
|