mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-26 17:47:17 +00:00
Compare commits
1 Commits
DynamicGro
...
rizumu/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e34506515 |
@@ -51,7 +51,8 @@ const mockHandleNativeDrop = vi.fn()
|
||||
vi.mock('@/composables/node/useNodeDragToCanvas', () => ({
|
||||
useNodeDragToCanvas: () => ({
|
||||
startDrag: mockStartDrag,
|
||||
handleNativeDrop: mockHandleNativeDrop
|
||||
handleNativeDrop: mockHandleNativeDrop,
|
||||
isDragging: ref(false)
|
||||
})
|
||||
}))
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ vi.mock(
|
||||
})
|
||||
)
|
||||
|
||||
// startDrag/cancelDrag toggle the litegraph ghost-placement flag; stub the
|
||||
// store so the composable runs without an active Pinia in this component test.
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: vi.fn(() => ({ isGhostPlacing: false, canvas: undefined }))
|
||||
}))
|
||||
|
||||
const nodeDef = fromPartial<ComfyNodeDefImpl>({ name: 'TestNode' })
|
||||
|
||||
function moveMouse(clientX: number, clientY: number) {
|
||||
|
||||
@@ -13,15 +13,19 @@ vi.mock('@/platform/settings/settingStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
const { mockStartDrag, mockHandleNativeDrop } = vi.hoisted(() => ({
|
||||
mockStartDrag: vi.fn(),
|
||||
mockHandleNativeDrop: vi.fn()
|
||||
}))
|
||||
const { mockStartDrag, mockHandleNativeDrop, mockIsPlacingNode } = vi.hoisted(
|
||||
() => ({
|
||||
mockStartDrag: vi.fn(),
|
||||
mockHandleNativeDrop: vi.fn(),
|
||||
mockIsPlacingNode: { value: false }
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/composables/node/useNodeDragToCanvas', () => ({
|
||||
useNodeDragToCanvas: () => ({
|
||||
startDrag: mockStartDrag,
|
||||
handleNativeDrop: mockHandleNativeDrop
|
||||
handleNativeDrop: mockHandleNativeDrop,
|
||||
isDragging: mockIsPlacingNode
|
||||
})
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Mock } from 'vitest'
|
||||
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import type { useNodeDragToCanvas as UseNodeDragToCanvasType } from './useNodeDragToCanvas'
|
||||
@@ -7,30 +8,33 @@ const {
|
||||
mockAddNodeOnGraph,
|
||||
mockConvertEventToCanvasOffset,
|
||||
mockSelectItems,
|
||||
mockCanvas,
|
||||
mockToastAdd
|
||||
mockCanvasStore,
|
||||
mockToastAdd,
|
||||
canvasElement
|
||||
} = vi.hoisted(() => {
|
||||
const canvasElement = document.createElement(
|
||||
'canvas'
|
||||
) as HTMLCanvasElement & { getBoundingClientRect: Mock }
|
||||
canvasElement.getBoundingClientRect = vi.fn()
|
||||
const mockConvertEventToCanvasOffset = vi.fn()
|
||||
const mockSelectItems = vi.fn()
|
||||
const mockCanvas = {
|
||||
canvas: canvasElement,
|
||||
convertEventToCanvasOffset: mockConvertEventToCanvasOffset,
|
||||
selectItems: mockSelectItems
|
||||
}
|
||||
return {
|
||||
mockAddNodeOnGraph: vi.fn(),
|
||||
mockConvertEventToCanvasOffset,
|
||||
mockSelectItems,
|
||||
mockToastAdd: vi.fn(),
|
||||
mockCanvas: {
|
||||
canvas: {
|
||||
getBoundingClientRect: vi.fn()
|
||||
},
|
||||
convertEventToCanvasOffset: mockConvertEventToCanvasOffset,
|
||||
selectItems: mockSelectItems
|
||||
}
|
||||
canvasElement,
|
||||
mockCanvasStore: { canvas: mockCanvas, isGhostPlacing: false }
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: vi.fn(() => ({
|
||||
canvas: mockCanvas
|
||||
}))
|
||||
useCanvasStore: vi.fn(() => mockCanvasStore)
|
||||
}))
|
||||
|
||||
vi.mock('@/services/litegraphService', () => ({
|
||||
@@ -45,8 +49,11 @@ vi.mock('@/platform/updates/common/toastStore', () => ({
|
||||
|
||||
vi.mock('@/i18n', () => ({ t: (key: string) => key }))
|
||||
|
||||
const CANVAS_RECT = { left: 0, right: 500, top: 0, bottom: 500 }
|
||||
|
||||
describe('useNodeDragToCanvas', () => {
|
||||
let useNodeDragToCanvas: typeof UseNodeDragToCanvasType
|
||||
let panelElement: HTMLElement
|
||||
|
||||
const mockNodeDef = {
|
||||
name: 'TestNode',
|
||||
@@ -57,6 +64,11 @@ describe('useNodeDragToCanvas', () => {
|
||||
vi.resetModules()
|
||||
vi.resetAllMocks()
|
||||
|
||||
document.body.appendChild(canvasElement)
|
||||
panelElement = document.createElement('div')
|
||||
document.body.appendChild(panelElement)
|
||||
mockCanvasStore.isGhostPlacing = false
|
||||
|
||||
const module = await import('./useNodeDragToCanvas')
|
||||
useNodeDragToCanvas = module.useNodeDragToCanvas
|
||||
})
|
||||
@@ -64,9 +76,24 @@ describe('useNodeDragToCanvas', () => {
|
||||
afterEach(() => {
|
||||
const { cancelDrag } = useNodeDragToCanvas()
|
||||
cancelDrag()
|
||||
canvasElement.remove()
|
||||
panelElement.remove()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
// The canvas is full-bleed under the sidebar/properties panels, so the click
|
||||
// path commits based on the event target rather than geometry. Dispatch on the
|
||||
// real element so `isCanvasTarget` (canvas.contains(target)) behaves as in the app.
|
||||
function dispatchPointerUp(
|
||||
x: number,
|
||||
y: number,
|
||||
target: EventTarget = canvasElement
|
||||
) {
|
||||
target.dispatchEvent(
|
||||
new PointerEvent('pointerup', { clientX: x, clientY: y, bubbles: true })
|
||||
)
|
||||
}
|
||||
|
||||
describe('startDrag', () => {
|
||||
it('should set isDragging to true and store the node definition', () => {
|
||||
const { isDragging, draggedNode, startDrag } = useNodeDragToCanvas()
|
||||
@@ -96,6 +123,27 @@ describe('useNodeDragToCanvas', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('ghost placement flag', () => {
|
||||
it('should mark ghost placement active for the duration of the drag', () => {
|
||||
const { startDrag, cancelDrag } = useNodeDragToCanvas()
|
||||
|
||||
startDrag(mockNodeDef)
|
||||
expect(mockCanvasStore.isGhostPlacing).toBe(true)
|
||||
|
||||
cancelDrag()
|
||||
expect(mockCanvasStore.isGhostPlacing).toBe(false)
|
||||
})
|
||||
|
||||
it('should not clear ghost placement when cancelling without a drag', () => {
|
||||
mockCanvasStore.isGhostPlacing = true
|
||||
const { cancelDrag } = useNodeDragToCanvas()
|
||||
|
||||
cancelDrag()
|
||||
|
||||
expect(mockCanvasStore.isGhostPlacing).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('drag listener lifecycle', () => {
|
||||
it('should attach document listeners on startDrag', () => {
|
||||
const addEventListenerSpy = vi.spyOn(document, 'addEventListener')
|
||||
@@ -167,47 +215,42 @@ describe('useNodeDragToCanvas', () => {
|
||||
})
|
||||
|
||||
describe('endDrag behavior', () => {
|
||||
it('should add node when pointer is over canvas', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
it('should add node when released over the canvas', () => {
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
|
||||
|
||||
const { startDrag } = useNodeDragToCanvas()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
const pointerEvent = new PointerEvent('pointerup', {
|
||||
clientX: 250,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
document.dispatchEvent(pointerEvent)
|
||||
dispatchPointerUp(250, 250)
|
||||
|
||||
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
|
||||
})
|
||||
it('should not add node when released outside the canvas', () => {
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
|
||||
const { startDrag, isDragging } = useNodeDragToCanvas()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
const pointerEvent = new PointerEvent('pointerup', {
|
||||
clientX: 600,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
document.dispatchEvent(pointerEvent)
|
||||
dispatchPointerUp(600, 250, panelElement)
|
||||
|
||||
expect(mockAddNodeOnGraph).not.toHaveBeenCalled()
|
||||
expect(isDragging.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should not add node when released over a panel within canvas bounds', () => {
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
|
||||
const { startDrag, isDragging } = useNodeDragToCanvas()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
// FE-688: the panel overlays the full-bleed canvas, so a release at a
|
||||
// point inside the canvas rect but on the panel must not place a hidden
|
||||
// node behind the panel.
|
||||
dispatchPointerUp(250, 250, panelElement)
|
||||
|
||||
expect(mockAddNodeOnGraph).not.toHaveBeenCalled()
|
||||
expect(isDragging.value).toBe(false)
|
||||
@@ -236,12 +279,7 @@ describe('useNodeDragToCanvas', () => {
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
|
||||
const placedNode = { id: 1 }
|
||||
mockAddNodeOnGraph.mockReturnValue(placedNode)
|
||||
@@ -249,24 +287,13 @@ describe('useNodeDragToCanvas', () => {
|
||||
const { startDrag } = useNodeDragToCanvas()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
document.dispatchEvent(
|
||||
new PointerEvent('pointerup', {
|
||||
clientX: 250,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
)
|
||||
dispatchPointerUp(250, 250)
|
||||
|
||||
expect(mockSelectItems).toHaveBeenCalledWith([placedNode])
|
||||
})
|
||||
|
||||
it('should apply the requested widget values to the placed node', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
|
||||
const widget = { name: 'ckpt_name', value: '' }
|
||||
mockAddNodeOnGraph.mockReturnValue({ id: 1, widgets: [widget] })
|
||||
@@ -276,24 +303,13 @@ describe('useNodeDragToCanvas', () => {
|
||||
widgetValues: { ckpt_name: 'model.safetensors' }
|
||||
})
|
||||
|
||||
document.dispatchEvent(
|
||||
new PointerEvent('pointerup', {
|
||||
clientX: 250,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
)
|
||||
dispatchPointerUp(250, 250)
|
||||
|
||||
expect(widget.value).toBe('model.safetensors')
|
||||
})
|
||||
|
||||
it('should warn but still place the node when a requested widget is missing', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
|
||||
const placedNode = { id: 1, widgets: [] }
|
||||
mockAddNodeOnGraph.mockReturnValue(placedNode)
|
||||
@@ -306,13 +322,7 @@ describe('useNodeDragToCanvas', () => {
|
||||
widgetValues: { ckpt_name: 'model.safetensors' }
|
||||
})
|
||||
|
||||
document.dispatchEvent(
|
||||
new PointerEvent('pointerup', {
|
||||
clientX: 250,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
)
|
||||
dispatchPointerUp(250, 250)
|
||||
|
||||
expect(mockSelectItems).toHaveBeenCalledWith([placedNode])
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
@@ -327,12 +337,7 @@ describe('useNodeDragToCanvas', () => {
|
||||
})
|
||||
|
||||
it('should show an error toast when the graph fails to add the node', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
|
||||
mockAddNodeOnGraph.mockReturnValue(null)
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
@@ -340,13 +345,7 @@ describe('useNodeDragToCanvas', () => {
|
||||
const { startDrag } = useNodeDragToCanvas()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
document.dispatchEvent(
|
||||
new PointerEvent('pointerup', {
|
||||
clientX: 250,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
)
|
||||
dispatchPointerUp(250, 250)
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -357,12 +356,7 @@ describe('useNodeDragToCanvas', () => {
|
||||
})
|
||||
|
||||
it('should not call selectItems when graph returns no node', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
|
||||
mockAddNodeOnGraph.mockReturnValue(null)
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
@@ -370,35 +364,19 @@ describe('useNodeDragToCanvas', () => {
|
||||
const { startDrag } = useNodeDragToCanvas()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
document.dispatchEvent(
|
||||
new PointerEvent('pointerup', {
|
||||
clientX: 250,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
)
|
||||
dispatchPointerUp(250, 250)
|
||||
|
||||
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
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
|
||||
|
||||
const { startDrag, isDragging } = useNodeDragToCanvas()
|
||||
startDrag(mockNodeDef, { mode: 'native' })
|
||||
|
||||
const pointerEvent = new PointerEvent('pointerup', {
|
||||
clientX: 250,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
document.dispatchEvent(pointerEvent)
|
||||
dispatchPointerUp(250, 250)
|
||||
|
||||
expect(mockAddNodeOnGraph).not.toHaveBeenCalled()
|
||||
expect(isDragging.value).toBe(true)
|
||||
@@ -407,12 +385,7 @@ describe('useNodeDragToCanvas', () => {
|
||||
|
||||
describe('handleNativeDrop', () => {
|
||||
it('should add node when drop position is over canvas', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([200, 200])
|
||||
|
||||
const { startDrag, handleNativeDrop } = useNodeDragToCanvas()
|
||||
@@ -426,12 +399,7 @@ describe('useNodeDragToCanvas', () => {
|
||||
})
|
||||
|
||||
it('should not add node when drop position is outside canvas', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
|
||||
const { startDrag, handleNativeDrop, isDragging } = useNodeDragToCanvas()
|
||||
|
||||
@@ -443,12 +411,7 @@ describe('useNodeDragToCanvas', () => {
|
||||
})
|
||||
|
||||
it('should not add node when dragMode is click', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([200, 200])
|
||||
|
||||
const { startDrag, handleNativeDrop } = useNodeDragToCanvas()
|
||||
@@ -460,12 +423,7 @@ describe('useNodeDragToCanvas', () => {
|
||||
})
|
||||
|
||||
it('should reset drag state after drop', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([200, 200])
|
||||
|
||||
const { startDrag, handleNativeDrop, isDragging } = useNodeDragToCanvas()
|
||||
@@ -478,7 +436,11 @@ describe('useNodeDragToCanvas', () => {
|
||||
})
|
||||
|
||||
describe('blockCommitPointerDown', () => {
|
||||
function dispatchPointerDown(x: number, y: number) {
|
||||
function dispatchPointerDown(
|
||||
x: number,
|
||||
y: number,
|
||||
target: EventTarget = canvasElement
|
||||
) {
|
||||
const event = new PointerEvent('pointerdown', {
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
@@ -486,17 +448,12 @@ describe('useNodeDragToCanvas', () => {
|
||||
cancelable: true
|
||||
})
|
||||
const stopSpy = vi.spyOn(event, 'stopImmediatePropagation')
|
||||
document.dispatchEvent(event)
|
||||
target.dispatchEvent(event)
|
||||
return stopSpy
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
})
|
||||
|
||||
it('should stop propagation when in click-drag mode over canvas', () => {
|
||||
@@ -521,22 +478,17 @@ describe('useNodeDragToCanvas', () => {
|
||||
expect(dispatchPointerDown(250, 250)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not stop propagation when pointer is outside canvas', () => {
|
||||
it('should not stop propagation when pointer is over a panel', () => {
|
||||
const { startDrag } = useNodeDragToCanvas()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
expect(dispatchPointerDown(600, 250)).not.toHaveBeenCalled()
|
||||
expect(dispatchPointerDown(250, 250, panelElement)).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('native drag position tracking', () => {
|
||||
beforeEach(() => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
canvasElement.getBoundingClientRect.mockReturnValue(CANVAS_RECT)
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([300, 300])
|
||||
})
|
||||
|
||||
|
||||
@@ -66,6 +66,17 @@ function isOverCanvas(clientX: number, clientY: number): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
// The canvas is full-bleed and sidebar/properties panels are pointer-events-auto
|
||||
// overlays painted on top of it, so a point inside the canvas rect can still be
|
||||
// over a panel. Hit-test the actual event target instead, mirroring how native
|
||||
// drag treats the canvas as its only drop target: releasing over a panel cancels.
|
||||
function isCanvasTarget(target: EventTarget | null): boolean {
|
||||
const canvasElement = useCanvasStore().canvas?.canvas
|
||||
return (
|
||||
!!canvasElement && target instanceof Node && canvasElement.contains(target)
|
||||
)
|
||||
}
|
||||
|
||||
function addNodeAtPosition(clientX: number, clientY: number): boolean {
|
||||
const nodeDef = draggedNode.value
|
||||
if (!nodeDef) return false
|
||||
@@ -101,7 +112,7 @@ function endDrag(e: PointerEvent) {
|
||||
if (dragMode.value !== 'click') return
|
||||
|
||||
try {
|
||||
addNodeAtPosition(e.clientX, e.clientY)
|
||||
if (isCanvasTarget(e.target)) addNodeAtPosition(e.clientX, e.clientY)
|
||||
} finally {
|
||||
cancelDrag()
|
||||
}
|
||||
@@ -114,7 +125,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
// Prevent LiteGraph's empty-canvas hit-test from deselecting the placed node on pointerup.
|
||||
function blockCommitPointerDown(e: PointerEvent) {
|
||||
if (!isDragging.value || dragMode.value !== 'click') return
|
||||
if (!isOverCanvas(e.clientX, e.clientY)) return
|
||||
if (!isCanvasTarget(e.target)) return
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
@@ -139,6 +150,7 @@ function cleanupGlobalListeners() {
|
||||
}
|
||||
|
||||
function cancelDrag() {
|
||||
if (isDragging.value) useCanvasStore().isGhostPlacing = false
|
||||
isDragging.value = false
|
||||
draggedNode.value = null
|
||||
dragMode.value = 'click'
|
||||
@@ -162,6 +174,10 @@ export function useNodeDragToCanvas() {
|
||||
dragMode.value = mode
|
||||
pendingWidgetValues.value = widgetValues
|
||||
pendingSource.value = source
|
||||
// Reuse the litegraph ghost-placement flag: Vue nodes render inert while
|
||||
// it is set, so the release hit-tests the canvas instead of an existing
|
||||
// node's DOM and placement over occupied areas isn't silently cancelled.
|
||||
useCanvasStore().isGhostPlacing = true
|
||||
setupGlobalListeners()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ import { useNodePreviewAndDrag } from './useNodePreviewAndDrag'
|
||||
|
||||
const mockStartDrag = vi.fn()
|
||||
const mockHandleNativeDrop = vi.fn()
|
||||
const mockIsPlacingNode = ref(false)
|
||||
|
||||
vi.mock('@/composables/node/useNodeDragToCanvas', () => ({
|
||||
useNodeDragToCanvas: () => ({
|
||||
startDrag: mockStartDrag,
|
||||
handleNativeDrop: mockHandleNativeDrop
|
||||
handleNativeDrop: mockHandleNativeDrop,
|
||||
isDragging: mockIsPlacingNode
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -29,6 +31,7 @@ describe('useNodePreviewAndDrag', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsPlacingNode.value = false
|
||||
})
|
||||
|
||||
describe('initial state', () => {
|
||||
@@ -52,6 +55,17 @@ describe('useNodePreviewAndDrag', () => {
|
||||
result.isDragging.value = true
|
||||
expect(result.showPreview.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should hide preview while a node is being placed elsewhere', () => {
|
||||
const nodeDef = ref<ComfyNodeDefImpl | undefined>(mockNodeDef)
|
||||
const result = useNodePreviewAndDrag(nodeDef)
|
||||
|
||||
result.isHovered.value = true
|
||||
expect(result.showPreview.value).toBe(true)
|
||||
|
||||
mockIsPlacingNode.value = true
|
||||
expect(result.showPreview.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleMouseEnter', () => {
|
||||
|
||||
@@ -25,7 +25,11 @@ export function useNodePreviewAndDrag(
|
||||
nodeDef: Ref<ComfyNodeDefImpl | undefined>,
|
||||
panelRef?: Ref<HTMLElement | null>
|
||||
): UseNodePreviewAndDragReturn {
|
||||
const { startDrag, handleNativeDrop } = useNodeDragToCanvas()
|
||||
const {
|
||||
startDrag,
|
||||
handleNativeDrop,
|
||||
isDragging: isPlacingNode
|
||||
} = useNodeDragToCanvas()
|
||||
const settingStore = useSettingStore()
|
||||
const sidebarLocation = computed<'left' | 'right'>(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
@@ -34,7 +38,11 @@ export function useNodePreviewAndDrag(
|
||||
const previewRef = ref<HTMLElement | null>(null)
|
||||
const isHovered = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const showPreview = computed(() => isHovered.value && !isDragging.value)
|
||||
// Hide the hover preview while a node is being placed (click or drag) so it
|
||||
// doesn't compete with the cursor-following placement preview.
|
||||
const showPreview = computed(
|
||||
() => isHovered.value && !isDragging.value && !isPlacingNode.value
|
||||
)
|
||||
|
||||
const nodePreviewStyle = ref<CSSProperties>({
|
||||
position: 'fixed',
|
||||
|
||||
Reference in New Issue
Block a user