mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-07 08:30:06 +00:00
## Summary Integrated Vue node components with canvas panning mode to prevent UI interference during navigation. ## Changes - **What**: Added [canCapturePointerEvents](https://docs.comfy.org/guide/vue-nodes) computed property to `useCanvasInteractions` composable that checks canvas read-only state - **What**: Modified Vue node components (LGraphNode, NodeWidgets) to conditionally handle pointer events based on canvas navigation mode - **What**: Updated node event handlers to respect panning mode and forward events to canvas when appropriate ## Review Focus Event forwarding logic in panning mode and pointer event capture state management across Vue node hierarchy. ```mermaid graph TD A[User Interaction] --> B{Canvas in Panning Mode?} B -->|Yes| C[Forward to Canvas] B -->|No| D[Handle in Vue Component] C --> E[Canvas Navigation] D --> F[Node Selection/Widget Interaction] G[canCapturePointerEvents] --> H{read_only === false} H -->|Yes| I[Allow Vue Events] H -->|No| J[Block Vue Events] style A fill:#f9f9f9,stroke:#333,color:#000 style E fill:#f9f9f9,stroke:#333,color:#000 style F fill:#f9f9f9,stroke:#333,color:#000 style I fill:#e1f5fe,stroke:#01579b,color:#000 style J fill:#ffebee,stroke:#c62828,color:#000 ``` ## Screenshots https://github.com/user-attachments/assets/00dc5e4a-2b56-43be-b92e-eaf511e52542 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5574-Make-Vue-nodes-read-only-when-in-panning-mode-26f6d73d3650818c951cd82c8fe58972) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
260 lines
7.6 KiB
TypeScript
260 lines
7.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { computed, ref } from 'vue'
|
|
|
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
|
import type { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
|
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
|
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
|
|
|
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
|
useCanvasStore: vi.fn()
|
|
}))
|
|
|
|
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
|
|
useCanvasInteractions: vi.fn()
|
|
}))
|
|
|
|
vi.mock('@/renderer/core/layout/operations/layoutMutations', () => ({
|
|
useLayoutMutations: vi.fn()
|
|
}))
|
|
|
|
vi.mock('@/composables/graph/useGraphNodeManager', () => ({
|
|
useGraphNodeManager: vi.fn()
|
|
}))
|
|
|
|
function createMockCanvas(): Pick<
|
|
LGraphCanvas,
|
|
'select' | 'deselect' | 'deselectAll'
|
|
> {
|
|
return {
|
|
select: vi.fn(),
|
|
deselect: vi.fn(),
|
|
deselectAll: vi.fn()
|
|
}
|
|
}
|
|
|
|
function createMockNode(): Pick<LGraphNode, 'id' | 'selected' | 'flags'> {
|
|
return {
|
|
id: 'node-1',
|
|
selected: false,
|
|
flags: { pinned: false }
|
|
}
|
|
}
|
|
|
|
function createMockNodeManager(
|
|
node: Pick<LGraphNode, 'id' | 'selected' | 'flags'>
|
|
) {
|
|
return {
|
|
getNode: vi.fn().mockReturnValue(node) as ReturnType<
|
|
typeof useGraphNodeManager
|
|
>['getNode']
|
|
}
|
|
}
|
|
|
|
function createMockCanvasStore(
|
|
canvas: Pick<LGraphCanvas, 'select' | 'deselect' | 'deselectAll'>
|
|
): Pick<
|
|
ReturnType<typeof useCanvasStore>,
|
|
'canvas' | 'selectedItems' | 'updateSelectedItems'
|
|
> {
|
|
return {
|
|
canvas: canvas as LGraphCanvas,
|
|
selectedItems: [],
|
|
updateSelectedItems: vi.fn()
|
|
}
|
|
}
|
|
|
|
function createMockLayoutMutations(): Pick<
|
|
ReturnType<typeof useLayoutMutations>,
|
|
'setSource' | 'bringNodeToFront'
|
|
> {
|
|
return {
|
|
setSource: vi.fn(),
|
|
bringNodeToFront: vi.fn()
|
|
}
|
|
}
|
|
|
|
function createMockCanvasInteractions(): Pick<
|
|
ReturnType<typeof useCanvasInteractions>,
|
|
'shouldHandleNodePointerEvents'
|
|
> {
|
|
return {
|
|
shouldHandleNodePointerEvents: computed(() => true) // Default to allowing pointer events
|
|
}
|
|
}
|
|
|
|
describe('useNodeEventHandlers', () => {
|
|
let mockCanvas: ReturnType<typeof createMockCanvas>
|
|
let mockNode: ReturnType<typeof createMockNode>
|
|
let mockNodeManager: ReturnType<typeof createMockNodeManager>
|
|
let mockCanvasStore: ReturnType<typeof createMockCanvasStore>
|
|
let mockLayoutMutations: ReturnType<typeof createMockLayoutMutations>
|
|
let mockCanvasInteractions: ReturnType<typeof createMockCanvasInteractions>
|
|
|
|
const testNodeData: VueNodeData = {
|
|
id: 'node-1',
|
|
title: 'Test Node',
|
|
type: 'test',
|
|
mode: 0,
|
|
selected: false,
|
|
executing: false
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
mockNode = createMockNode()
|
|
mockCanvas = createMockCanvas()
|
|
mockNodeManager = createMockNodeManager(mockNode)
|
|
mockCanvasStore = createMockCanvasStore(mockCanvas)
|
|
mockLayoutMutations = createMockLayoutMutations()
|
|
mockCanvasInteractions = createMockCanvasInteractions()
|
|
|
|
vi.mocked(useCanvasStore).mockReturnValue(
|
|
mockCanvasStore as ReturnType<typeof useCanvasStore>
|
|
)
|
|
vi.mocked(useLayoutMutations).mockReturnValue(
|
|
mockLayoutMutations as ReturnType<typeof useLayoutMutations>
|
|
)
|
|
vi.mocked(useCanvasInteractions).mockReturnValue(
|
|
mockCanvasInteractions as ReturnType<typeof useCanvasInteractions>
|
|
)
|
|
})
|
|
|
|
describe('handleNodeSelect', () => {
|
|
it('should select single node on regular click', () => {
|
|
const nodeManager = ref(mockNodeManager)
|
|
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
|
|
|
const event = new PointerEvent('pointerdown', {
|
|
bubbles: true,
|
|
ctrlKey: false,
|
|
metaKey: false
|
|
})
|
|
|
|
handleNodeSelect(event, testNodeData, false)
|
|
|
|
expect(mockCanvas.deselectAll).toHaveBeenCalledOnce()
|
|
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
|
|
expect(mockCanvasStore.updateSelectedItems).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('should toggle selection on ctrl+click', () => {
|
|
const nodeManager = ref(mockNodeManager)
|
|
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
|
|
|
// Test selecting unselected node with ctrl
|
|
mockNode.selected = false
|
|
|
|
const ctrlClickEvent = new PointerEvent('pointerdown', {
|
|
bubbles: true,
|
|
ctrlKey: true,
|
|
metaKey: false
|
|
})
|
|
|
|
handleNodeSelect(ctrlClickEvent, testNodeData, false)
|
|
|
|
expect(mockCanvas.deselectAll).not.toHaveBeenCalled()
|
|
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
|
|
})
|
|
|
|
it('should deselect on ctrl+click of selected node', () => {
|
|
const nodeManager = ref(mockNodeManager)
|
|
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
|
|
|
// Test deselecting selected node with ctrl
|
|
mockNode.selected = true
|
|
|
|
const ctrlClickEvent = new PointerEvent('pointerdown', {
|
|
bubbles: true,
|
|
ctrlKey: true,
|
|
metaKey: false
|
|
})
|
|
|
|
handleNodeSelect(ctrlClickEvent, testNodeData, false)
|
|
|
|
expect(mockCanvas.deselect).toHaveBeenCalledWith(mockNode)
|
|
expect(mockCanvas.select).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle meta key (Cmd) on Mac', () => {
|
|
const nodeManager = ref(mockNodeManager)
|
|
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
|
|
|
mockNode.selected = false
|
|
|
|
const metaClickEvent = new PointerEvent('pointerdown', {
|
|
bubbles: true,
|
|
ctrlKey: false,
|
|
metaKey: true
|
|
})
|
|
|
|
handleNodeSelect(metaClickEvent, testNodeData, false)
|
|
|
|
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
|
|
expect(mockCanvas.deselectAll).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should bring node to front when not pinned', () => {
|
|
const nodeManager = ref(mockNodeManager)
|
|
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
|
|
|
mockNode.flags.pinned = false
|
|
|
|
const event = new PointerEvent('pointerdown')
|
|
handleNodeSelect(event, testNodeData, false)
|
|
|
|
expect(mockLayoutMutations.bringNodeToFront).toHaveBeenCalledWith(
|
|
'node-1'
|
|
)
|
|
})
|
|
|
|
it('should not bring pinned node to front', () => {
|
|
const nodeManager = ref(mockNodeManager)
|
|
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
|
|
|
mockNode.flags.pinned = true
|
|
|
|
const event = new PointerEvent('pointerdown')
|
|
handleNodeSelect(event, testNodeData, false)
|
|
|
|
expect(mockLayoutMutations.bringNodeToFront).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle missing canvas gracefully', () => {
|
|
const nodeManager = ref(mockNodeManager)
|
|
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
|
|
|
mockCanvasStore.canvas = null
|
|
|
|
const event = new PointerEvent('pointerdown')
|
|
expect(() => {
|
|
handleNodeSelect(event, testNodeData, false)
|
|
}).not.toThrow()
|
|
|
|
expect(mockCanvas.select).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle missing node gracefully', () => {
|
|
const nodeManager = ref(mockNodeManager)
|
|
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
|
|
|
vi.mocked(mockNodeManager.getNode).mockReturnValue(undefined)
|
|
|
|
const event = new PointerEvent('pointerdown')
|
|
const nodeData = {
|
|
id: 'missing-node',
|
|
title: 'Missing Node',
|
|
type: 'test'
|
|
} as any
|
|
|
|
expect(() => {
|
|
handleNodeSelect(event, nodeData, false)
|
|
}).not.toThrow()
|
|
|
|
expect(mockCanvas.select).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|