Compare commits

...

2 Commits

Author SHA1 Message Date
Arjan Singh
aee2366496 [test] simplify test code for drag handling 2025-09-27 17:09:32 -07:00
Arjan Singh
4dd4334287 [refactor] simplify vue nodes drag handling 2025-09-27 17:01:18 -07:00
5 changed files with 338 additions and 196 deletions

View File

@@ -38,7 +38,7 @@
backgroundColor: nodeBodyBackgroundColor, backgroundColor: nodeBodyBackgroundColor,
opacity: nodeOpacity opacity: nodeOpacity
}, },
dragStyle { cursor: nodeStyle.cursor }
]" ]"
v-bind="pointerHandlers" v-bind="pointerHandlers"
@wheel="handleWheel" @wheel="handleWheel"
@@ -262,8 +262,10 @@ onErrorCaptured((error) => {
}) })
// Use layout system for node position and dragging // Use layout system for node position and dragging
const { position, size, zIndex, resize } = useNodeLayout(() => nodeData.id) const { position, size, zIndex, resize, nodeStyle } = useNodeLayout(
const { pointerHandlers, isDragging, dragStyle } = useNodePointerInteractions( () => nodeData.id
)
const { pointerHandlers, isDragging } = useNodePointerInteractions(
() => nodeData, () => nodeData,
handleNodeSelect handleNodeSelect
) )

View File

@@ -1,8 +1,9 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue' import { computed, ref } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions' import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
// Mock the dependencies // Mock the dependencies
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({ vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
@@ -12,19 +13,27 @@ vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
}) })
})) }))
// Mock the layout system
vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({ vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({
useNodeLayout: () => ({ useNodeLayout: vi.fn()
startDrag: vi.fn(),
endDrag: vi.fn().mockResolvedValue(undefined),
handleDrag: vi.fn().mockResolvedValue(undefined)
})
})) }))
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({ const createBaseNodeLayoutMock = () => ({
layoutStore: { isDragging: ref(false),
isDraggingVueNodes: ref(false) startDrag: vi.fn(() => true),
} endDrag: vi.fn(() => Promise.resolve()),
})) handleDrag: vi.fn(),
layoutRef: ref(null),
position: computed(() => ({ x: 0, y: 0 })),
size: computed(() => ({ width: 200, height: 100 })),
bounds: computed(() => ({ x: 0, y: 0, width: 200, height: 100 })),
isVisible: computed(() => true),
zIndex: computed(() => 0),
moveTo: vi.fn(),
resize: vi.fn(),
nodeStyle: computed(() => ({ position: 'absolute' as const }))
})
vi.mock('@/renderer/core/layout/store/layoutStore')
const createMockVueNodeData = ( const createMockVueNodeData = (
overrides: Partial<VueNodeData> = {} overrides: Partial<VueNodeData> = {}
@@ -42,28 +51,38 @@ const createMockVueNodeData = (
}) })
const createPointerEvent = ( const createPointerEvent = (
eventType: string, type: string,
overrides: Partial<PointerEventInit> = {} overrides: Partial<PointerEvent> = {}
): PointerEvent => { ): PointerEvent => {
return new PointerEvent(eventType, { const event = new PointerEvent(type, {
pointerId: 1, pointerId: 1,
button: 0,
clientX: 100, clientX: 100,
clientY: 100, clientY: 100,
button: 0,
...overrides ...overrides
}) })
Object.defineProperty(event, 'target', {
value: {
setPointerCapture: vi.fn(),
releasePointerCapture: vi.fn()
},
writable: false
})
return event
} }
const createMouseEvent = ( const createMouseEvent = (
eventType: string, type: string,
overrides: Partial<MouseEventInit> = {} overrides: Partial<MouseEvent> = {}
): MouseEvent => { ): MouseEvent => {
return new MouseEvent(eventType, { const event = new MouseEvent(type, {
button: 2, // Right click
clientX: 100, clientX: 100,
clientY: 100, clientY: 100,
button: 0,
...overrides ...overrides
}) })
event.preventDefault = vi.fn()
return event
} }
describe('useNodePointerInteractions', () => { describe('useNodePointerInteractions', () => {
@@ -71,144 +90,241 @@ describe('useNodePointerInteractions', () => {
vi.clearAllMocks() vi.clearAllMocks()
}) })
it('should only start drag on left-click', async () => { afterEach(() => {
const mockNodeData = createMockVueNodeData() vi.restoreAllMocks()
const mockOnPointerUp = vi.fn()
const { pointerHandlers } = useNodePointerInteractions(
ref(mockNodeData),
mockOnPointerUp
)
// Right-click should not start drag
const rightClickEvent = createPointerEvent('pointerdown', { button: 2 })
pointerHandlers.onPointerdown(rightClickEvent)
expect(mockOnPointerUp).not.toHaveBeenCalled()
// Left-click should start drag and emit callback
const leftClickEvent = createPointerEvent('pointerdown', { button: 0 })
pointerHandlers.onPointerdown(leftClickEvent)
const pointerUpEvent = createPointerEvent('pointerup')
pointerHandlers.onPointerup(pointerUpEvent)
expect(mockOnPointerUp).toHaveBeenCalledWith(
pointerUpEvent,
mockNodeData,
false // wasDragging = false (same position)
)
}) })
it('should distinguish drag from click based on distance threshold', async () => { describe('DrJKL single source of truth architecture', () => {
const mockNodeData = createMockVueNodeData() it('should use useNodeLayout.isDragging as the single authority', () => {
const mockOnPointerUp = vi.fn() const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
const { pointerHandlers } = useNodePointerInteractions( const mockIsDragging = ref(false)
ref(mockNodeData), const testMock = createBaseNodeLayoutMock()
mockOnPointerUp testMock.isDragging = mockIsDragging
)
// Test drag (distance > 4px) vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
pointerHandlers.onPointerdown(
createPointerEvent('pointerdown', { clientX: 100, clientY: 100 })
)
const dragUpEvent = createPointerEvent('pointerup', { const { isDragging } = useNodePointerInteractions(
clientX: 200, ref(mockNodeData),
clientY: 200 mockOnPointerUp
)
expect(isDragging).toBe(mockIsDragging)
}) })
pointerHandlers.onPointerup(dragUpEvent)
expect(mockOnPointerUp).toHaveBeenCalledWith( it('should eliminate coordination object entirely', () => {
dragUpEvent, const mockNodeData = createMockVueNodeData()
mockNodeData, const mockOnPointerUp = vi.fn()
true
)
mockOnPointerUp.mockClear() vi.mocked(useNodeLayout).mockReturnValueOnce(createBaseNodeLayoutMock())
// Test click (same position) const result = useNodePointerInteractions(
const samePos = { clientX: 100, clientY: 100 } ref(mockNodeData),
pointerHandlers.onPointerdown(createPointerEvent('pointerdown', samePos)) mockOnPointerUp
)
const clickUpEvent = createPointerEvent('pointerup', samePos) expect(result.isDragging).toBeDefined()
pointerHandlers.onPointerup(clickUpEvent) expect(result.pointerHandlers).toBeDefined()
expect(result.stopWatcher).toBeDefined()
})
expect(mockOnPointerUp).toHaveBeenCalledWith( it('should use pure Vue reactivity for global state sync', async () => {
clickUpEvent, const mockNodeData = createMockVueNodeData()
mockNodeData, const mockOnPointerUp = vi.fn()
false
) const mockStartDrag = vi.fn(() => true)
const testMock = createBaseNodeLayoutMock()
testMock.startDrag = mockStartDrag
vi.mocked(useNodeLayout).mockReturnValue(testMock)
const { pointerHandlers, isDragging } = useNodePointerInteractions(
ref(mockNodeData),
mockOnPointerUp
)
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
expect(mockStartDrag).toHaveBeenCalledWith(
expect.objectContaining({
type: 'pointerdown',
button: 0
})
)
expect(isDragging).toBe(testMock.isDragging)
})
it('should make startDrag return success boolean to fix race condition', () => {
const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
const mockIsDragging = ref(false)
const mockStartDrag = vi.fn().mockImplementation(() => {
mockIsDragging.value = true
return true
})
const testMock = createBaseNodeLayoutMock()
testMock.isDragging = mockIsDragging
testMock.startDrag = mockStartDrag
vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
const { pointerHandlers } = useNodePointerInteractions(
ref(mockNodeData),
mockOnPointerUp
)
// Test 1: startDrag should be called and return success
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
expect(mockStartDrag).toHaveBeenCalled()
expect(mockStartDrag).toHaveReturnedWith(true)
// Reset for next test
vi.clearAllMocks()
mockIsDragging.value = false
// Test 2: If startDrag fails, drag state shouldn't be set
mockStartDrag.mockReturnValue(false)
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
expect(mockStartDrag).toHaveBeenCalled()
expect(mockIsDragging.value).toBe(false)
})
it('should have clean Vue-native implementation without manual sync', () => {
const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
const mockIsDragging = ref(false)
const testMock = createBaseNodeLayoutMock()
testMock.isDragging = mockIsDragging
vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
const result = useNodePointerInteractions(
ref(mockNodeData),
mockOnPointerUp
)
expect(result.isDragging).toBeDefined()
expect(result.pointerHandlers).toBeDefined()
expect(result.stopWatcher).toBeDefined()
expect(result.isDragging.value).toBe(false)
mockIsDragging.value = true
expect(result.isDragging.value).toBe(true)
})
}) })
it('should handle drag termination via cancel and context menu', async () => { // Essential integration tests
const mockNodeData = createMockVueNodeData() describe('basic functionality', () => {
const mockOnPointerUp = vi.fn() it('should only start drag on left-click', async () => {
const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
const { pointerHandlers } = useNodePointerInteractions( const mockStartDrag = vi.fn()
ref(mockNodeData), const testMock = createBaseNodeLayoutMock()
mockOnPointerUp testMock.startDrag = mockStartDrag
)
// Test pointer cancel vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
pointerHandlers.onPointercancel(createPointerEvent('pointercancel'))
// Should not emit callback on cancel const { pointerHandlers } = useNodePointerInteractions(
expect(mockOnPointerUp).not.toHaveBeenCalled() ref(mockNodeData),
mockOnPointerUp
)
// Test context menu during drag prevents default // Right-click should not start drag
pointerHandlers.onPointerdown(createPointerEvent('pointerdown')) const rightClick = createPointerEvent('pointerdown', { button: 2 })
pointerHandlers.onPointerdown(rightClick)
expect(mockStartDrag).not.toHaveBeenCalled()
const contextMenuEvent = createMouseEvent('contextmenu') // Left-click should start drag
const preventDefaultSpy = vi.spyOn(contextMenuEvent, 'preventDefault') const leftClick = createPointerEvent('pointerdown', { button: 0 })
pointerHandlers.onPointerdown(leftClick)
expect(mockStartDrag).toHaveBeenCalled()
})
pointerHandlers.onContextmenu(contextMenuEvent) it('should distinguish drag from click based on distance threshold', async () => {
const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
expect(preventDefaultSpy).toHaveBeenCalled() const mockIsDragging = ref(false)
}) const testMock = createBaseNodeLayoutMock()
testMock.isDragging = mockIsDragging
it('should not emit callback when nodeData becomes null', async () => { vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
const nodeDataRef = ref<VueNodeData | null>(mockNodeData)
const { pointerHandlers } = useNodePointerInteractions( const { pointerHandlers } = useNodePointerInteractions(
nodeDataRef, ref(mockNodeData),
mockOnPointerUp mockOnPointerUp
) )
pointerHandlers.onPointerdown(createPointerEvent('pointerdown')) // Start at 100, 100
pointerHandlers.onPointerdown(
createPointerEvent('pointerdown', { clientX: 100, clientY: 100 })
)
// Clear nodeData before pointerup // Move just 2 pixels (below threshold)
nodeDataRef.value = null pointerHandlers.onPointerup(
createPointerEvent('pointerup', { clientX: 102, clientY: 102 })
)
pointerHandlers.onPointerup(createPointerEvent('pointerup')) // Should be considered a click, not drag
expect(mockOnPointerUp).toHaveBeenCalledWith(
expect.any(Object),
mockNodeData,
false // wasDragging = false
)
})
expect(mockOnPointerUp).not.toHaveBeenCalled() it('should handle drag termination via cancel and context menu', async () => {
}) const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
it('should integrate with layout store dragging state', async () => { const mockIsDragging = ref(true)
const mockNodeData = createMockVueNodeData() const testMock = createBaseNodeLayoutMock()
const mockOnPointerUp = vi.fn() testMock.isDragging = mockIsDragging
const { layoutStore } = await import(
'@/renderer/core/layout/store/layoutStore'
)
const { pointerHandlers } = useNodePointerInteractions( vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
ref(mockNodeData),
mockOnPointerUp
)
// Start drag const { pointerHandlers } = useNodePointerInteractions(
pointerHandlers.onPointerdown(createPointerEvent('pointerdown')) ref(mockNodeData),
await nextTick() mockOnPointerUp
expect(layoutStore.isDraggingVueNodes.value).toBe(true) )
// End drag const contextMenu = createMouseEvent('contextmenu')
pointerHandlers.onPointercancel(createPointerEvent('pointercancel')) pointerHandlers.onContextmenu(contextMenu)
await nextTick()
expect(layoutStore.isDraggingVueNodes.value).toBe(false) expect(contextMenu.preventDefault).toHaveBeenCalled()
mockIsDragging.value = false
const contextMenuNotDragging = createMouseEvent('contextmenu')
pointerHandlers.onContextmenu(contextMenuNotDragging)
expect(contextMenuNotDragging.preventDefault).not.toHaveBeenCalled()
})
it('should not emit callback when nodeData becomes null', async () => {
const mockOnPointerUp = vi.fn()
const mockIsDragging = ref(false)
const testMock = createBaseNodeLayoutMock()
testMock.isDragging = mockIsDragging
vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
// Start with null nodeData
const { pointerHandlers } = useNodePointerInteractions(
ref(null),
mockOnPointerUp
)
// Should not crash or call callback
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
pointerHandlers.onPointerup(createPointerEvent('pointerup'))
expect(mockOnPointerUp).not.toHaveBeenCalled()
})
}) })
}) })

View File

@@ -1,12 +1,18 @@
import { type MaybeRefOrGetter, computed, onUnmounted, ref, toValue } from 'vue' import { type MaybeRefOrGetter, computed, ref, toValue, watch } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout' import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
interface Position {
x: number
y: number
}
// Treat tiny pointer jitter as a click, not a drag // Treat tiny pointer jitter as a click, not a drag
const DRAG_THRESHOLD_PX = 4 const DRAG_THRESHOLD_PX = 4
const DRAG_THRESHOLD_SQUARED = DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX
export function useNodePointerInteractions( export function useNodePointerInteractions(
nodeDataMaybe: MaybeRefOrGetter<VueNodeData | null>, nodeDataMaybe: MaybeRefOrGetter<VueNodeData | null>,
@@ -29,68 +35,50 @@ export function useNodePointerInteractions(
// Avoid potential null access during component initialization // Avoid potential null access during component initialization
const nodeIdComputed = computed(() => nodeData.value?.id ?? '') const nodeIdComputed = computed(() => nodeData.value?.id ?? '')
const { startDrag, endDrag, handleDrag } = useNodeLayout(nodeIdComputed) const { startDrag, endDrag, handleDrag, isDragging } =
useNodeLayout(nodeIdComputed)
// Use canvas interactions for proper wheel event handling and pointer event capture control // Use canvas interactions for proper wheel event handling and pointer event capture control
const { forwardEventToCanvas, shouldHandleNodePointerEvents } = const { forwardEventToCanvas, shouldHandleNodePointerEvents } =
useCanvasInteractions() useCanvasInteractions()
// Drag state for styling const startPosition = ref<Position>({ x: 0, y: 0 })
const isDragging = ref(false)
const dragStyle = computed(() => { const stopWatcher = watch(
if (nodeData.value?.flags?.pinned) { isDragging,
return { cursor: 'default' } (dragging) => {
} layoutStore.isDraggingVueNodes.value = dragging
return { cursor: isDragging.value ? 'grabbing' : 'grab' } },
}) { immediate: true }
const startPosition = ref({ x: 0, y: 0 }) )
const handlePointerDown = (event: PointerEvent) => { const handlePointerDown = (event: PointerEvent) => {
if (!nodeData.value) { if (!nodeData.value) {
console.warn( console.warn(
'LGraphNode: nodeData is null/undefined in handlePointerDown' 'useNodePointerInteractions: nodeData is null in handlePointerDown'
) )
return return
} }
// Only start drag on left-click (button 0) if (event.button !== 0) return
if (event.button !== 0) {
return
}
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
if (!shouldHandleNodePointerEvents.value) { if (!shouldHandleNodePointerEvents.value) {
forwardEventToCanvas(event) forwardEventToCanvas(event)
return return
} }
// Don't allow dragging if node is pinned (but still record position for selection)
startPosition.value = { x: event.clientX, y: event.clientY } startPosition.value = { x: event.clientX, y: event.clientY }
if (nodeData.value.flags?.pinned) {
const dragStarted = startDrag(event)
if (!dragStarted) {
startPosition.value = { x: 0, y: 0 }
return return
} }
// Start drag using layout system
isDragging.value = true
// Set Vue node dragging state for selection toolbox
layoutStore.isDraggingVueNodes.value = true
startDrag(event)
} }
const handlePointerMove = (event: PointerEvent) => { const handlePointerMove = (event: PointerEvent) => {
if (isDragging.value) { if (!isDragging.value) return
void handleDrag(event)
}
}
/** handleDrag(event)
* Centralized cleanup function for drag state
* Ensures consistent cleanup across all drag termination scenarios
*/
const cleanupDragState = () => {
isDragging.value = false
layoutStore.isDraggingVueNodes.value = false
} }
/** /**
@@ -100,10 +88,13 @@ export function useNodePointerInteractions(
const safeDragEnd = async (event: PointerEvent): Promise<void> => { const safeDragEnd = async (event: PointerEvent): Promise<void> => {
try { try {
await endDrag(event) await endDrag(event)
} catch (error) { } catch (error: unknown) {
console.error('Error during endDrag:', error) const errorMessage =
} finally { error instanceof Error ? error.message : String(error)
cleanupDragState() console.error(
'useNodePointerInteractions: Error during endDrag -',
errorMessage
)
} }
} }
@@ -111,9 +102,13 @@ export function useNodePointerInteractions(
* Common drag termination handler with fallback cleanup * Common drag termination handler with fallback cleanup
*/ */
const handleDragTermination = (event: PointerEvent, errorContext: string) => { const handleDragTermination = (event: PointerEvent, errorContext: string) => {
safeDragEnd(event).catch((error) => { safeDragEnd(event).catch((error: unknown) => {
console.error(`Failed to complete ${errorContext}:`, error) const errorMessage =
cleanupDragState() // Fallback cleanup error instanceof Error ? error.message : String(error)
console.error(
`useNodePointerInteractions: Failed to complete ${errorContext} -`,
errorMessage
)
}) })
} }
@@ -122,18 +117,18 @@ export function useNodePointerInteractions(
handleDragTermination(event, 'drag end') handleDragTermination(event, 'drag end')
} }
// Don't emit node-click when canvas is in panning mode - forward to canvas instead
if (!shouldHandleNodePointerEvents.value) { if (!shouldHandleNodePointerEvents.value) {
forwardEventToCanvas(event) forwardEventToCanvas(event)
return return
} }
if (!nodeData?.value) return
// Emit node-click for selection handling in GraphCanvas // Emit node-click for selection handling in GraphCanvas
const dx = event.clientX - startPosition.value.x const dx = event.clientX - startPosition.value.x
const dy = event.clientY - startPosition.value.y const dy = event.clientY - startPosition.value.y
const wasDragging = Math.hypot(dx, dy) > DRAG_THRESHOLD_PX const wasDragging = dx * dx + dy * dy > DRAG_THRESHOLD_SQUARED
if (!nodeData?.value) return
onPointerUp(event, nodeData.value, wasDragging) onPointerUp(event, nodeData.value, wasDragging)
} }
@@ -143,6 +138,7 @@ export function useNodePointerInteractions(
*/ */
const handlePointerCancel = (event: PointerEvent) => { const handlePointerCancel = (event: PointerEvent) => {
if (!isDragging.value) return if (!isDragging.value) return
handleDragTermination(event, 'drag cancellation') handleDragTermination(event, 'drag cancellation')
} }
@@ -154,16 +150,8 @@ export function useNodePointerInteractions(
if (!isDragging.value) return if (!isDragging.value) return
event.preventDefault() event.preventDefault()
// Simply cleanup state without calling endDrag to avoid synthetic event creation
cleanupDragState()
} }
// Cleanup on unmount to prevent resource leaks
onUnmounted(() => {
if (!isDragging.value) return
cleanupDragState()
})
const pointerHandlers = { const pointerHandlers = {
onPointerdown: handlePointerDown, onPointerdown: handlePointerDown,
onPointermove: handlePointerMove, onPointermove: handlePointerMove,
@@ -174,7 +162,7 @@ export function useNodePointerInteractions(
return { return {
isDragging, isDragging,
dragStyle, pointerHandlers,
pointerHandlers stopWatcher
} }
} }

View File

@@ -58,9 +58,10 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {
/** /**
* Start dragging the node * Start dragging the node
* @returns {boolean} True if drag started successfully, false otherwise
*/ */
function startDrag(event: PointerEvent) { function startDrag(event: PointerEvent): boolean {
if (!layoutRef.value || !transformState) return if (!layoutRef.value || !transformState) return false
isDragging.value = true isDragging.value = true
dragStartPos = { ...position.value } dragStartPos = { ...position.value }
@@ -88,8 +89,10 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {
mutations.setSource(LayoutSource.Vue) mutations.setSource(LayoutSource.Vue)
// Capture pointer // Capture pointer
if (!(event.target instanceof HTMLElement)) return if (!(event.target instanceof HTMLElement)) return false
event.target.setPointerCapture(event.pointerId) event.target.setPointerCapture(event.pointerId)
return true
} }
/** /**

View File

@@ -49,14 +49,47 @@ vi.mock('@/composables/useErrorHandling', () => ({
vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({ vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({
useNodeLayout: () => ({ useNodeLayout: () => ({
position: { x: 100, y: 50 }, layoutRef: computed(() => null),
size: { width: 200, height: 100 }, position: computed(() => ({ x: 100, y: 50 })),
size: computed(() => ({ width: 200, height: 100 })),
bounds: computed(() => ({ x: 100, y: 50, width: 200, height: 100 })),
isVisible: computed(() => true),
zIndex: computed(() => 0),
moveTo: vi.fn(),
resize: vi.fn(),
startDrag: vi.fn(), startDrag: vi.fn(),
handleDrag: vi.fn(), handleDrag: vi.fn(),
endDrag: vi.fn() endDrag: vi.fn(),
isDragging: computed(() => false),
nodeStyle: computed(() => ({ cursor: 'grab' }))
}) })
})) }))
vi.mock(
'@/renderer/extensions/vueNodes/composables/useNodePointerInteractions',
() => ({
useNodePointerInteractions: (nodeDataMaybe: any, onPointerUp: any) => ({
isDragging: computed(() => false),
pointerHandlers: {
onPointerdown: vi.fn(),
onPointermove: vi.fn(),
onPointerup: (event: PointerEvent) => {
const nodeData =
typeof nodeDataMaybe === 'function'
? nodeDataMaybe()
: nodeDataMaybe
if (nodeData && onPointerUp) {
onPointerUp(event, nodeData, false) // false = wasDragging
}
},
onPointercancel: vi.fn(),
onContextmenu: vi.fn()
},
stopWatcher: vi.fn()
})
})
)
vi.mock( vi.mock(
'@/renderer/extensions/vueNodes/execution/useNodeExecutionState', '@/renderer/extensions/vueNodes/execution/useNodeExecutionState',
() => ({ () => ({