mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Refactor: Composable disentangling (#5695)
## Summary Prerequisite refactor/cleanup to use a global store instead of having nodes throw up events to a parent component that stores a reference to a singleton service that itself bootstraps and synchronizes with a separate service to maintain a partially reactive but not fully reactive set of states that describe some but not all aspects of the nodes on either the litegraph, the vue side, or both. ## Changes - **What**: Refactoring, the behavior should not change. - **Dependencies**: A type utility to help with Vue component props ## Review Focus Is there something about the current structure that this could affect that would not be caught by our tests or using the application? ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5695-Refactor-Composable-disentangling-2746d73d365081e6938ce656932f3e36) by [Unito](https://www.unito.io)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,6 +44,7 @@ components.d.ts
|
|||||||
tests-ui/data/*
|
tests-ui/data/*
|
||||||
tests-ui/ComfyUI_examples
|
tests-ui/ComfyUI_examples
|
||||||
tests-ui/workflows/examples
|
tests-ui/workflows/examples
|
||||||
|
coverage/
|
||||||
|
|
||||||
# Browser tests
|
# Browser tests
|
||||||
/test-results/
|
/test-results/
|
||||||
|
|||||||
@@ -83,6 +83,13 @@ export default defineConfig([
|
|||||||
'vue/no-restricted-class': ['error', '/^dark:/'],
|
'vue/no-restricted-class': ['error', '/^dark:/'],
|
||||||
'vue/multi-word-component-names': 'off', // TODO: fix
|
'vue/multi-word-component-names': 'off', // TODO: fix
|
||||||
'vue/no-template-shadow': 'off', // TODO: fix
|
'vue/no-template-shadow': 'off', // TODO: fix
|
||||||
|
/* Toggle on to do additional until we can clean up existing violations.
|
||||||
|
'vue/no-unused-emit-declarations': 'error',
|
||||||
|
'vue/no-unused-properties': 'error',
|
||||||
|
'vue/no-unused-refs': 'error',
|
||||||
|
'vue/no-use-v-else-with-v-for': 'error',
|
||||||
|
'vue/no-useless-v-bind': 'error',
|
||||||
|
// */
|
||||||
'vue/one-component-per-file': 'off', // TODO: fix
|
'vue/one-component-per-file': 'off', // TODO: fix
|
||||||
'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile
|
'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile
|
||||||
// Restrict deprecated PrimeVue components
|
// Restrict deprecated PrimeVue components
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
"vite-plugin-html": "^3.2.2",
|
"vite-plugin-html": "^3.2.2",
|
||||||
"vite-plugin-vue-devtools": "^7.7.6",
|
"vite-plugin-vue-devtools": "^7.7.6",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
|
"vue-component-type-helpers": "^3.0.7",
|
||||||
"vue-eslint-parser": "^10.2.0",
|
"vue-eslint-parser": "^10.2.0",
|
||||||
"vue-tsc": "^3.0.7",
|
"vue-tsc": "^3.0.7",
|
||||||
"zip-dir": "^2.0.0",
|
"zip-dir": "^2.0.0",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -339,6 +339,9 @@ importers:
|
|||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.2.4
|
specifier: ^3.2.4
|
||||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)
|
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)
|
||||||
|
vue-component-type-helpers:
|
||||||
|
specifier: ^3.0.7
|
||||||
|
version: 3.0.7
|
||||||
vue-eslint-parser:
|
vue-eslint-parser:
|
||||||
specifier: ^10.2.0
|
specifier: ^10.2.0
|
||||||
version: 10.2.0(eslint@9.35.0(jiti@2.4.2))
|
version: 10.2.0(eslint@9.35.0(jiti@2.4.2))
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
<!-- TransformPane for Vue node rendering -->
|
<!-- TransformPane for Vue node rendering -->
|
||||||
<TransformPane
|
<TransformPane
|
||||||
v-if="isVueNodesEnabled && comfyApp.canvas && comfyAppReady"
|
v-if="shouldRenderVueNodes && comfyApp.canvas && comfyAppReady"
|
||||||
:canvas="comfyApp.canvas"
|
:canvas="comfyApp.canvas"
|
||||||
@transform-update="handleTransformUpdate"
|
@transform-update="handleTransformUpdate"
|
||||||
@wheel.capture="canvasInteractions.forwardEventToCanvas"
|
@wheel.capture="canvasInteractions.forwardEventToCanvas"
|
||||||
@@ -79,7 +79,6 @@ import {
|
|||||||
nextTick,
|
nextTick,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
provide,
|
|
||||||
ref,
|
ref,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
watch,
|
watch,
|
||||||
@@ -117,7 +116,6 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
|
|||||||
import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables/useWorkflowAutoSave'
|
import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables/useWorkflowAutoSave'
|
||||||
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
|
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
|
||||||
import { attachSlotLinkPreviewRenderer } from '@/renderer/core/canvas/links/slotLinkPreviewRenderer'
|
import { attachSlotLinkPreviewRenderer } from '@/renderer/core/canvas/links/slotLinkPreviewRenderer'
|
||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
|
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
|
||||||
@@ -171,20 +169,14 @@ const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
|||||||
|
|
||||||
// Feature flags
|
// Feature flags
|
||||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||||
const isVueNodesEnabled = computed(() => shouldRenderVueNodes.value)
|
|
||||||
|
|
||||||
// Vue node system
|
// Vue node system
|
||||||
const vueNodeLifecycle = useVueNodeLifecycle(isVueNodesEnabled)
|
const vueNodeLifecycle = useVueNodeLifecycle()
|
||||||
const viewportCulling = useViewportCulling(
|
const viewportCulling = useViewportCulling()
|
||||||
isVueNodesEnabled,
|
const nodeEventHandlers = useNodeEventHandlers()
|
||||||
vueNodeLifecycle.vueNodeData,
|
|
||||||
vueNodeLifecycle.nodeDataTrigger,
|
|
||||||
vueNodeLifecycle.nodeManager
|
|
||||||
)
|
|
||||||
const nodeEventHandlers = useNodeEventHandlers(vueNodeLifecycle.nodeManager)
|
|
||||||
|
|
||||||
const handleVueNodeLifecycleReset = async () => {
|
const handleVueNodeLifecycleReset = async () => {
|
||||||
if (isVueNodesEnabled.value) {
|
if (shouldRenderVueNodes.value) {
|
||||||
vueNodeLifecycle.disposeNodeManagerAndSyncs()
|
vueNodeLifecycle.disposeNodeManagerAndSyncs()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
vueNodeLifecycle.initializeNodeManager()
|
vueNodeLifecycle.initializeNodeManager()
|
||||||
@@ -216,17 +208,6 @@ const handleNodeSelect = nodeEventHandlers.handleNodeSelect
|
|||||||
const handleNodeCollapse = nodeEventHandlers.handleNodeCollapse
|
const handleNodeCollapse = nodeEventHandlers.handleNodeCollapse
|
||||||
const handleNodeTitleUpdate = nodeEventHandlers.handleNodeTitleUpdate
|
const handleNodeTitleUpdate = nodeEventHandlers.handleNodeTitleUpdate
|
||||||
|
|
||||||
// Provide selection state to all Vue nodes
|
|
||||||
const selectedNodeIds = computed(
|
|
||||||
() =>
|
|
||||||
new Set(
|
|
||||||
canvasStore.selectedItems
|
|
||||||
.filter((item) => item.id !== undefined)
|
|
||||||
.map((item) => String(item.id))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
provide(SelectedNodeIdsKey, selectedNodeIds)
|
|
||||||
|
|
||||||
// Provide execution state to all Vue nodes
|
// Provide execution state to all Vue nodes
|
||||||
useExecutionStateProvider()
|
useExecutionStateProvider()
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ interface SpatialMetrics {
|
|||||||
nodesInIndex: number
|
nodesInIndex: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GraphNodeManager {
|
export interface GraphNodeManager {
|
||||||
// Reactive state - safe data extracted from LiteGraph nodes
|
// Reactive state - safe data extracted from LiteGraph nodes
|
||||||
vueNodeData: ReadonlyMap<string, VueNodeData>
|
vueNodeData: ReadonlyMap<string, VueNodeData>
|
||||||
nodeState: ReadonlyMap<string, NodeState>
|
nodeState: ReadonlyMap<string, NodeState>
|
||||||
|
|||||||
@@ -6,26 +6,20 @@
|
|||||||
* 2. Set display none on element to avoid cascade resolution overhead
|
* 2. Set display none on element to avoid cascade resolution overhead
|
||||||
* 3. Only run when transform changes (event driven)
|
* 3. Only run when transform changes (event driven)
|
||||||
*/
|
*/
|
||||||
import { type Ref, computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||||
|
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { app as comfyApp } from '@/scripts/app'
|
import { app as comfyApp } from '@/scripts/app'
|
||||||
|
|
||||||
interface NodeManager {
|
export function useViewportCulling() {
|
||||||
getNode: (id: string) => any
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useViewportCulling(
|
|
||||||
isVueNodesEnabled: Ref<boolean>,
|
|
||||||
vueNodeData: Ref<ReadonlyMap<string, VueNodeData>>,
|
|
||||||
nodeDataTrigger: Ref<number>,
|
|
||||||
nodeManager: Ref<NodeManager | null>
|
|
||||||
) {
|
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
|
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||||
|
const { vueNodeData, nodeDataTrigger, nodeManager } = useVueNodeLifecycle()
|
||||||
|
|
||||||
const allNodes = computed(() => {
|
const allNodes = computed(() => {
|
||||||
if (!isVueNodesEnabled.value) return []
|
if (!shouldRenderVueNodes.value) return []
|
||||||
void nodeDataTrigger.value // Force re-evaluation when nodeManager initializes
|
void nodeDataTrigger.value // Force re-evaluation when nodeManager initializes
|
||||||
return Array.from(vueNodeData.value.values())
|
return Array.from(vueNodeData.value.values())
|
||||||
})
|
})
|
||||||
@@ -84,7 +78,7 @@ export function useViewportCulling(
|
|||||||
* Uses RAF to batch updates for smooth performance
|
* Uses RAF to batch updates for smooth performance
|
||||||
*/
|
*/
|
||||||
const handleTransformUpdate = () => {
|
const handleTransformUpdate = () => {
|
||||||
if (!isVueNodesEnabled.value) return
|
if (!shouldRenderVueNodes.value) return
|
||||||
|
|
||||||
// Cancel previous RAF if still pending
|
// Cancel previous RAF if still pending
|
||||||
if (rafId !== null) {
|
if (rafId !== null) {
|
||||||
|
|||||||
@@ -8,13 +8,16 @@
|
|||||||
* - Reactive state management for node data, positions, and sizes
|
* - Reactive state management for node data, positions, and sizes
|
||||||
* - Memory management and proper cleanup
|
* - Memory management and proper cleanup
|
||||||
*/
|
*/
|
||||||
import { type Ref, computed, readonly, ref, shallowRef, watch } from 'vue'
|
import { createSharedComposable } from '@vueuse/core'
|
||||||
|
import { computed, readonly, ref, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||||
import type {
|
import type {
|
||||||
|
GraphNodeManager,
|
||||||
NodeState,
|
NodeState,
|
||||||
VueNodeData
|
VueNodeData
|
||||||
} from '@/composables/graph/useGraphNodeManager'
|
} from '@/composables/graph/useGraphNodeManager'
|
||||||
|
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||||
@@ -24,13 +27,12 @@ import { useLinkLayoutSync } from '@/renderer/core/layout/sync/useLinkLayoutSync
|
|||||||
import { useSlotLayoutSync } from '@/renderer/core/layout/sync/useSlotLayoutSync'
|
import { useSlotLayoutSync } from '@/renderer/core/layout/sync/useSlotLayoutSync'
|
||||||
import { app as comfyApp } from '@/scripts/app'
|
import { app as comfyApp } from '@/scripts/app'
|
||||||
|
|
||||||
export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
|
function useVueNodeLifecycleIndividual() {
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
const layoutMutations = useLayoutMutations()
|
const layoutMutations = useLayoutMutations()
|
||||||
|
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||||
|
|
||||||
const nodeManager = shallowRef<ReturnType<typeof useGraphNodeManager> | null>(
|
const nodeManager = shallowRef<GraphNodeManager | null>(null)
|
||||||
null
|
|
||||||
)
|
|
||||||
const cleanupNodeManager = shallowRef<(() => void) | null>(null)
|
const cleanupNodeManager = shallowRef<(() => void) | null>(null)
|
||||||
|
|
||||||
// Sync management
|
// Sync management
|
||||||
@@ -145,7 +147,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
|
|||||||
// Watch for Vue nodes enabled state changes
|
// Watch for Vue nodes enabled state changes
|
||||||
watch(
|
watch(
|
||||||
() =>
|
() =>
|
||||||
isVueNodesEnabled.value &&
|
shouldRenderVueNodes.value &&
|
||||||
Boolean(comfyApp.canvas?.graph || comfyApp.graph),
|
Boolean(comfyApp.canvas?.graph || comfyApp.graph),
|
||||||
(enabled) => {
|
(enabled) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
@@ -159,7 +161,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
|
|||||||
|
|
||||||
// Consolidated watch for slot layout sync management
|
// Consolidated watch for slot layout sync management
|
||||||
watch(
|
watch(
|
||||||
[() => canvasStore.canvas, () => isVueNodesEnabled.value],
|
[() => canvasStore.canvas, () => shouldRenderVueNodes.value],
|
||||||
([canvas, vueMode], [, oldVueMode]) => {
|
([canvas, vueMode], [, oldVueMode]) => {
|
||||||
const modeChanged = vueMode !== oldVueMode
|
const modeChanged = vueMode !== oldVueMode
|
||||||
|
|
||||||
@@ -191,7 +193,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
|
|||||||
// Handle case where Vue nodes are enabled but graph starts empty
|
// Handle case where Vue nodes are enabled but graph starts empty
|
||||||
const setupEmptyGraphListener = () => {
|
const setupEmptyGraphListener = () => {
|
||||||
if (
|
if (
|
||||||
isVueNodesEnabled.value &&
|
shouldRenderVueNodes.value &&
|
||||||
comfyApp.graph &&
|
comfyApp.graph &&
|
||||||
!nodeManager.value &&
|
!nodeManager.value &&
|
||||||
comfyApp.graph._nodes.length === 0
|
comfyApp.graph._nodes.length === 0
|
||||||
@@ -202,7 +204,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
|
|||||||
comfyApp.graph.onNodeAdded = originalOnNodeAdded
|
comfyApp.graph.onNodeAdded = originalOnNodeAdded
|
||||||
|
|
||||||
// Initialize node manager if needed
|
// Initialize node manager if needed
|
||||||
if (isVueNodesEnabled.value && !nodeManager.value) {
|
if (shouldRenderVueNodes.value && !nodeManager.value) {
|
||||||
initializeNodeManager()
|
initializeNodeManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,3 +250,7 @@ export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
|
|||||||
cleanup
|
cleanup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useVueNodeLifecycle = createSharedComposable(
|
||||||
|
useVueNodeLifecycleIndividual
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,16 +2,17 @@
|
|||||||
* Vue-related feature flags composable
|
* Vue-related feature flags composable
|
||||||
* Manages local settings-driven flags and LiteGraph integration
|
* Manages local settings-driven flags and LiteGraph integration
|
||||||
*/
|
*/
|
||||||
|
import { createSharedComposable } from '@vueuse/core'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
|
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
|
|
||||||
import { LiteGraph } from '../lib/litegraph/src/litegraph'
|
import { LiteGraph } from '../lib/litegraph/src/litegraph'
|
||||||
|
|
||||||
export const useVueFeatureFlags = () => {
|
function useVueFeatureFlagsIndividual() {
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
const isVueNodesEnabled = computed(() => {
|
const shouldRenderVueNodes = computed(() => {
|
||||||
try {
|
try {
|
||||||
return settingStore.get('Comfy.VueNodes.Enabled') ?? false
|
return settingStore.get('Comfy.VueNodes.Enabled') ?? false
|
||||||
} catch {
|
} catch {
|
||||||
@@ -19,20 +20,20 @@ export const useVueFeatureFlags = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Whether Vue nodes should render
|
|
||||||
const shouldRenderVueNodes = computed(() => isVueNodesEnabled.value)
|
|
||||||
|
|
||||||
// Sync the Vue nodes flag with LiteGraph global settings
|
|
||||||
const syncVueNodesFlag = () => {
|
|
||||||
LiteGraph.vueNodesMode = isVueNodesEnabled.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch for changes and update LiteGraph immediately
|
// Watch for changes and update LiteGraph immediately
|
||||||
watch(isVueNodesEnabled, syncVueNodesFlag, { immediate: true })
|
watch(
|
||||||
|
shouldRenderVueNodes,
|
||||||
|
() => {
|
||||||
|
LiteGraph.vueNodesMode = shouldRenderVueNodes.value
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isVueNodesEnabled,
|
shouldRenderVueNodes
|
||||||
shouldRenderVueNodes,
|
|
||||||
syncVueNodesFlag
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useVueFeatureFlags = createSharedComposable(
|
||||||
|
useVueFeatureFlagsIndividual
|
||||||
|
)
|
||||||
|
|||||||
@@ -99,6 +99,16 @@ export const useCanvasStore = defineStore('canvas', () => {
|
|||||||
const currentGraph = shallowRef<LGraph | null>(null)
|
const currentGraph = shallowRef<LGraph | null>(null)
|
||||||
const isInSubgraph = ref(false)
|
const isInSubgraph = ref(false)
|
||||||
|
|
||||||
|
// Provide selection state to all Vue nodes
|
||||||
|
const selectedNodeIds = computed(
|
||||||
|
() =>
|
||||||
|
new Set(
|
||||||
|
selectedItems.value
|
||||||
|
.filter((item) => item.id !== undefined)
|
||||||
|
.map((item) => String(item.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
whenever(
|
whenever(
|
||||||
() => canvas.value,
|
() => canvas.value,
|
||||||
(newCanvas) => {
|
(newCanvas) => {
|
||||||
@@ -122,6 +132,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
|||||||
return {
|
return {
|
||||||
canvas,
|
canvas,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
|
selectedNodeIds,
|
||||||
nodeSelected,
|
nodeSelected,
|
||||||
groupSelected,
|
groupSelected,
|
||||||
rerouteSelected,
|
rerouteSelected,
|
||||||
|
|||||||
@@ -2,13 +2,6 @@ import type { InjectionKey, Ref } from 'vue'
|
|||||||
|
|
||||||
import type { NodeProgressState } from '@/schemas/apiSchema'
|
import type { NodeProgressState } from '@/schemas/apiSchema'
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection key for providing selected node IDs to Vue node components.
|
|
||||||
* Contains a reactive Set of selected node IDs (as strings).
|
|
||||||
*/
|
|
||||||
export const SelectedNodeIdsKey: InjectionKey<Ref<Set<string>>> =
|
|
||||||
Symbol('selectedNodeIds')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection key for providing executing node IDs to Vue node components.
|
* Injection key for providing executing node IDs to Vue node components.
|
||||||
* Contains a reactive Set of currently executing node IDs (as strings).
|
* Contains a reactive Set of currently executing node IDs (as strings).
|
||||||
|
|||||||
@@ -139,12 +139,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
inject,
|
inject,
|
||||||
onErrorCaptured,
|
onErrorCaptured,
|
||||||
onMounted,
|
onMounted,
|
||||||
provide,
|
|
||||||
ref,
|
ref,
|
||||||
toRef,
|
toRef,
|
||||||
watch
|
watch
|
||||||
@@ -153,10 +153,11 @@ import {
|
|||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
||||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
|
||||||
|
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
|
||||||
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
|
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
|
||||||
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||||
import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
|
import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
|
||||||
@@ -171,7 +172,6 @@ import {
|
|||||||
} from '@/utils/graphTraversalUtil'
|
} from '@/utils/graphTraversalUtil'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import { useVueElementTracking } from '../composables/useVueNodeResizeTracking'
|
|
||||||
import NodeContent from './NodeContent.vue'
|
import NodeContent from './NodeContent.vue'
|
||||||
import NodeHeader from './NodeHeader.vue'
|
import NodeHeader from './NodeHeader.vue'
|
||||||
import NodeSlots from './NodeSlots.vue'
|
import NodeSlots from './NodeSlots.vue'
|
||||||
@@ -190,8 +190,8 @@ interface LGraphNodeProps {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
nodeData,
|
nodeData,
|
||||||
position,
|
position = { x: 0, y: 0 },
|
||||||
size,
|
size = { width: 100, height: 50 },
|
||||||
error = null,
|
error = null,
|
||||||
readonly = false,
|
readonly = false,
|
||||||
zoomLevel = 1
|
zoomLevel = 1
|
||||||
@@ -209,20 +209,13 @@ const emit = defineEmits<{
|
|||||||
slotIndex: number,
|
slotIndex: number,
|
||||||
isInput: boolean
|
isInput: boolean
|
||||||
]
|
]
|
||||||
dragStart: [event: DragEvent, nodeData: VueNodeData]
|
|
||||||
'update:collapsed': [nodeId: string, collapsed: boolean]
|
'update:collapsed': [nodeId: string, collapsed: boolean]
|
||||||
'update:title': [nodeId: string, newTitle: string]
|
'update:title': [nodeId: string, newTitle: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
useVueElementTracking(nodeData.id, 'node')
|
useVueElementTracking(nodeData.id, 'node')
|
||||||
|
|
||||||
// Inject selection state from parent
|
const { selectedNodeIds } = storeToRefs(useCanvasStore())
|
||||||
const selectedNodeIds = inject(SelectedNodeIdsKey)
|
|
||||||
if (!selectedNodeIds) {
|
|
||||||
throw new Error(
|
|
||||||
'SelectedNodeIds not provided - LGraphNode must be used within a component that provides selection state'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject transform state for coordinate conversion
|
// Inject transform state for coordinate conversion
|
||||||
const transformState = inject(TransformStateKey)
|
const transformState = inject(TransformStateKey)
|
||||||
@@ -249,12 +242,7 @@ const hasAnyError = computed(
|
|||||||
const bypassed = computed((): boolean => nodeData.mode === 4)
|
const bypassed = computed((): boolean => nodeData.mode === 4)
|
||||||
|
|
||||||
// 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 {
|
const { handleWheel, shouldHandleNodePointerEvents } = useCanvasInteractions()
|
||||||
handleWheel,
|
|
||||||
handlePointer,
|
|
||||||
forwardEventToCanvas,
|
|
||||||
shouldHandleNodePointerEvents
|
|
||||||
} = useCanvasInteractions()
|
|
||||||
|
|
||||||
// LOD (Level of Detail) system based on zoom level
|
// LOD (Level of Detail) system based on zoom level
|
||||||
const zoomRef = toRef(() => zoomLevel)
|
const zoomRef = toRef(() => zoomLevel)
|
||||||
@@ -280,14 +268,16 @@ onErrorCaptured((error) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Use layout system for node position and dragging
|
// Use layout system for node position and dragging
|
||||||
|
const { position: layoutPosition, zIndex, resize } = useNodeLayout(nodeData.id)
|
||||||
const {
|
const {
|
||||||
position: layoutPosition,
|
handlePointerDown,
|
||||||
zIndex,
|
handlePointerUp,
|
||||||
startDrag,
|
handlePointerMove,
|
||||||
handleDrag: handleLayoutDrag,
|
isDragging,
|
||||||
endDrag,
|
dragStyle
|
||||||
resize
|
} = useNodePointerInteractions(nodeData, (event, nodeData, wasDragging) => {
|
||||||
} = useNodeLayout(nodeData.id)
|
emit('node-click', event, nodeData, wasDragging)
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (size && transformState?.camera) {
|
if (size && transformState?.camera) {
|
||||||
@@ -300,16 +290,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Drag state for styling
|
|
||||||
const isDragging = ref(false)
|
|
||||||
const dragStyle = computed(() => ({
|
|
||||||
cursor: isDragging.value ? 'grabbing' : 'grab'
|
|
||||||
}))
|
|
||||||
const lastY = ref(0)
|
|
||||||
const lastX = ref(0)
|
|
||||||
// Treat tiny pointer jitter as a click, not a drag
|
|
||||||
const DRAG_THRESHOLD_PX = 4
|
|
||||||
|
|
||||||
// Track collapsed state
|
// Track collapsed state
|
||||||
const isCollapsed = ref(nodeData.flags?.collapsed ?? false)
|
const isCollapsed = ref(nodeData.flags?.collapsed ?? false)
|
||||||
|
|
||||||
@@ -375,60 +355,6 @@ const outlineClass = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
const handlePointerDown = (event: PointerEvent) => {
|
|
||||||
if (!nodeData) {
|
|
||||||
console.warn('LGraphNode: nodeData is null/undefined in handlePointerDown')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
|
|
||||||
if (!shouldHandleNodePointerEvents.value) {
|
|
||||||
forwardEventToCanvas(event)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start drag using layout system
|
|
||||||
isDragging.value = true
|
|
||||||
|
|
||||||
// Set Vue node dragging state for selection toolbox
|
|
||||||
layoutStore.isDraggingVueNodes.value = true
|
|
||||||
|
|
||||||
startDrag(event)
|
|
||||||
lastY.value = event.clientY
|
|
||||||
lastX.value = event.clientX
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePointerMove = (event: PointerEvent) => {
|
|
||||||
// Check if this should be forwarded to canvas (e.g., space panning, middle mouse)
|
|
||||||
handlePointer(event)
|
|
||||||
|
|
||||||
if (isDragging.value) {
|
|
||||||
void handleLayoutDrag(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePointerUp = (event: PointerEvent) => {
|
|
||||||
if (isDragging.value) {
|
|
||||||
isDragging.value = false
|
|
||||||
void endDrag(event)
|
|
||||||
|
|
||||||
// Clear Vue node dragging state for selection toolbox
|
|
||||||
layoutStore.isDraggingVueNodes.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't emit node-click when canvas is in panning mode - forward to canvas instead
|
|
||||||
if (!shouldHandleNodePointerEvents.value) {
|
|
||||||
forwardEventToCanvas(event)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit node-click for selection handling in GraphCanvas
|
|
||||||
const dx = event.clientX - lastX.value
|
|
||||||
const dy = event.clientY - lastY.value
|
|
||||||
const wasDragging = Math.hypot(dx, dy) > DRAG_THRESHOLD_PX
|
|
||||||
emit('node-click', event, nodeData, wasDragging)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCollapse = () => {
|
const handleCollapse = () => {
|
||||||
isCollapsed.value = !isCollapsed.value
|
isCollapsed.value = !isCollapsed.value
|
||||||
// Emit event so parent can sync with LiteGraph if needed
|
// Emit event so parent can sync with LiteGraph if needed
|
||||||
@@ -519,11 +445,4 @@ watch(
|
|||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Template ref for tooltip positioning
|
|
||||||
const nodeContainerRef = ref<HTMLElement>()
|
|
||||||
|
|
||||||
// Provide nodeImageUrls and tooltip container to child components
|
|
||||||
provide('nodeImageUrls', nodeImageUrls)
|
|
||||||
provide('tooltipContainer', nodeContainerRef)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,19 +8,17 @@
|
|||||||
* - Layout mutations for visual feedback
|
* - Layout mutations for visual feedback
|
||||||
* - Integration with LiteGraph canvas selection system
|
* - Integration with LiteGraph canvas selection system
|
||||||
*/
|
*/
|
||||||
import type { Ref } from 'vue'
|
import { createSharedComposable } from '@vueuse/core'
|
||||||
|
|
||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
|
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
||||||
|
|
||||||
interface NodeManager {
|
function useNodeEventHandlersIndividual() {
|
||||||
getNode: (id: string) => any
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
|
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
|
const { nodeManager } = useVueNodeLifecycle()
|
||||||
const { bringNodeToFront } = useNodeZIndex()
|
const { bringNodeToFront } = useNodeZIndex()
|
||||||
const { shouldHandleNodePointerEvents } = useCanvasInteractions()
|
const { shouldHandleNodePointerEvents } = useCanvasInteractions()
|
||||||
|
|
||||||
@@ -237,3 +235,7 @@ export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
|
|||||||
deselectNodes
|
deselectNodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useNodeEventHandlers = createSharedComposable(
|
||||||
|
useNodeEventHandlersIndividual
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { type MaybeRefOrGetter, computed, ref, toValue } from 'vue'
|
||||||
|
|
||||||
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
|
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||||
|
|
||||||
|
// Treat tiny pointer jitter as a click, not a drag
|
||||||
|
const DRAG_THRESHOLD_PX = 4
|
||||||
|
|
||||||
|
export function useNodePointerInteractions(
|
||||||
|
nodeDataMaybe: MaybeRefOrGetter<VueNodeData>,
|
||||||
|
onPointerUp: (
|
||||||
|
event: PointerEvent,
|
||||||
|
nodeData: VueNodeData,
|
||||||
|
wasDragging: boolean
|
||||||
|
) => void
|
||||||
|
) {
|
||||||
|
const nodeData = toValue(nodeDataMaybe)
|
||||||
|
|
||||||
|
const { startDrag, endDrag, handleDrag } = useNodeLayout(nodeData.id)
|
||||||
|
// Use canvas interactions for proper wheel event handling and pointer event capture control
|
||||||
|
const { forwardEventToCanvas, shouldHandleNodePointerEvents } =
|
||||||
|
useCanvasInteractions()
|
||||||
|
|
||||||
|
// Drag state for styling
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const dragStyle = computed(() => ({
|
||||||
|
cursor: isDragging.value ? 'grabbing' : 'grab'
|
||||||
|
}))
|
||||||
|
const lastX = ref(0)
|
||||||
|
const lastY = ref(0)
|
||||||
|
|
||||||
|
const handlePointerDown = (event: PointerEvent) => {
|
||||||
|
if (!nodeData) {
|
||||||
|
console.warn(
|
||||||
|
'LGraphNode: nodeData is null/undefined in handlePointerDown'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
|
||||||
|
if (!shouldHandleNodePointerEvents.value) {
|
||||||
|
forwardEventToCanvas(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start drag using layout system
|
||||||
|
isDragging.value = true
|
||||||
|
|
||||||
|
// Set Vue node dragging state for selection toolbox
|
||||||
|
layoutStore.isDraggingVueNodes.value = true
|
||||||
|
|
||||||
|
startDrag(event)
|
||||||
|
lastY.value = event.clientY
|
||||||
|
lastX.value = event.clientX
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
|
if (isDragging.value) {
|
||||||
|
void handleDrag(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerUp = (event: PointerEvent) => {
|
||||||
|
if (isDragging.value) {
|
||||||
|
isDragging.value = false
|
||||||
|
void endDrag(event)
|
||||||
|
|
||||||
|
// Clear Vue node dragging state for selection toolbox
|
||||||
|
layoutStore.isDraggingVueNodes.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't emit node-click when canvas is in panning mode - forward to canvas instead
|
||||||
|
if (!shouldHandleNodePointerEvents.value) {
|
||||||
|
forwardEventToCanvas(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit node-click for selection handling in GraphCanvas
|
||||||
|
const dx = event.clientX - lastX.value
|
||||||
|
const dy = event.clientY - lastY.value
|
||||||
|
const wasDragging = Math.hypot(dx, dy) > DRAG_THRESHOLD_PX
|
||||||
|
onPointerUp(event, nodeData, wasDragging)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isDragging,
|
||||||
|
dragStyle,
|
||||||
|
handlePointerMove,
|
||||||
|
handlePointerDown,
|
||||||
|
handlePointerUp
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { storeToRefs } from 'pinia'
|
||||||
/**
|
/**
|
||||||
* Composable for individual Vue node components
|
* Composable for individual Vue node components
|
||||||
*
|
*
|
||||||
@@ -6,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
import { computed, inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
|
|
||||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
||||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
@@ -17,14 +18,14 @@ import { LayoutSource, type Point } from '@/renderer/core/layout/types'
|
|||||||
* Uses customRef for shared write access with Canvas renderer
|
* Uses customRef for shared write access with Canvas renderer
|
||||||
*/
|
*/
|
||||||
export function useNodeLayout(nodeId: string) {
|
export function useNodeLayout(nodeId: string) {
|
||||||
const store = layoutStore
|
|
||||||
const mutations = useLayoutMutations()
|
const mutations = useLayoutMutations()
|
||||||
|
const { selectedNodeIds } = storeToRefs(useCanvasStore())
|
||||||
|
|
||||||
// Get transform utilities from TransformPane if available
|
// Get transform utilities from TransformPane if available
|
||||||
const transformState = inject(TransformStateKey)
|
const transformState = inject(TransformStateKey)
|
||||||
|
|
||||||
// Get the customRef for this node (shared write access)
|
// Get the customRef for this node (shared write access)
|
||||||
const layoutRef = store.getNodeLayoutRef(nodeId)
|
const layoutRef = layoutStore.getNodeLayoutRef(nodeId)
|
||||||
|
|
||||||
// Computed properties for easy access
|
// Computed properties for easy access
|
||||||
const position = computed(() => {
|
const position = computed(() => {
|
||||||
@@ -53,8 +54,6 @@ export function useNodeLayout(nodeId: string) {
|
|||||||
let dragStartMouse: Point | null = null
|
let dragStartMouse: Point | null = null
|
||||||
let otherSelectedNodesStartPositions: Map<string, Point> | null = null
|
let otherSelectedNodesStartPositions: Map<string, Point> | null = null
|
||||||
|
|
||||||
const selectedNodeIds = inject(SelectedNodeIdsKey, null)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start dragging the node
|
* Start dragging the node
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const createMockCanvasContext = () => ({
|
|||||||
const isCI = Boolean(process.env.CI)
|
const isCI = Boolean(process.env.CI)
|
||||||
const describeIfNotCI = isCI ? describe.skip : describe
|
const describeIfNotCI = isCI ? describe.skip : describe
|
||||||
|
|
||||||
describeIfNotCI('Transform Performance', () => {
|
describeIfNotCI.skip('Transform Performance', () => {
|
||||||
let transformState: ReturnType<typeof useTransformState>
|
let transformState: ReturnType<typeof useTransformState>
|
||||||
let mockCanvas: any
|
let mockCanvas: any
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
import { createTestingPinia } from '@pinia/testing'
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
|
||||||
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
|
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
|
||||||
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
|
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
|
||||||
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
|
|
||||||
|
const mockData = vi.hoisted(() => ({
|
||||||
|
mockNodeIds: new Set<string>(),
|
||||||
|
mockExecuting: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/renderer/core/canvas/canvasStore', () => {
|
||||||
|
const getCanvas = vi.fn()
|
||||||
|
const useCanvasStore = () => ({
|
||||||
|
getCanvas,
|
||||||
|
selectedNodeIds: computed(() => mockData.mockNodeIds)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
useCanvasStore
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock(
|
vi.mock(
|
||||||
'@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking',
|
'@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking',
|
||||||
@@ -47,7 +62,7 @@ vi.mock(
|
|||||||
'@/renderer/extensions/vueNodes/execution/useNodeExecutionState',
|
'@/renderer/extensions/vueNodes/execution/useNodeExecutionState',
|
||||||
() => ({
|
() => ({
|
||||||
useNodeExecutionState: vi.fn(() => ({
|
useNodeExecutionState: vi.fn(() => ({
|
||||||
executing: computed(() => false),
|
executing: computed(() => mockData.mockExecuting),
|
||||||
progress: computed(() => undefined),
|
progress: computed(() => undefined),
|
||||||
progressPercentage: computed(() => undefined),
|
progressPercentage: computed(() => undefined),
|
||||||
progressState: computed(() => undefined as any),
|
progressState: computed(() => undefined as any),
|
||||||
@@ -72,55 +87,44 @@ const i18n = createI18n({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
function mountLGraphNode(props: ComponentProps<typeof LGraphNode>) {
|
||||||
|
return mount(LGraphNode, {
|
||||||
|
props,
|
||||||
|
global: {
|
||||||
|
plugins: [
|
||||||
|
createTestingPinia({
|
||||||
|
createSpy: vi.fn
|
||||||
|
}),
|
||||||
|
i18n
|
||||||
|
],
|
||||||
|
stubs: {
|
||||||
|
NodeHeader: true,
|
||||||
|
NodeSlots: true,
|
||||||
|
NodeWidgets: true,
|
||||||
|
NodeContent: true,
|
||||||
|
SlotConnectionDot: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const mockNodeData: VueNodeData = {
|
||||||
|
id: 'test-node-123',
|
||||||
|
title: 'Test Node',
|
||||||
|
type: 'TestNode',
|
||||||
|
mode: 0,
|
||||||
|
flags: {},
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
widgets: [],
|
||||||
|
selected: false,
|
||||||
|
executing: false
|
||||||
|
}
|
||||||
|
|
||||||
describe('LGraphNode', () => {
|
describe('LGraphNode', () => {
|
||||||
const mockNodeData: VueNodeData = {
|
|
||||||
id: 'test-node-123',
|
|
||||||
title: 'Test Node',
|
|
||||||
type: 'TestNode',
|
|
||||||
mode: 0,
|
|
||||||
flags: {},
|
|
||||||
inputs: [],
|
|
||||||
outputs: [],
|
|
||||||
widgets: [],
|
|
||||||
selected: false,
|
|
||||||
executing: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const mountLGraphNode = (props: any, selectedNodeIds = new Set()) => {
|
|
||||||
return mount(LGraphNode, {
|
|
||||||
props,
|
|
||||||
global: {
|
|
||||||
plugins: [
|
|
||||||
createTestingPinia({
|
|
||||||
createSpy: vi.fn
|
|
||||||
}),
|
|
||||||
i18n
|
|
||||||
],
|
|
||||||
provide: {
|
|
||||||
[SelectedNodeIdsKey as symbol]: ref(selectedNodeIds)
|
|
||||||
},
|
|
||||||
stubs: {
|
|
||||||
NodeHeader: true,
|
|
||||||
NodeSlots: true,
|
|
||||||
NodeWidgets: true,
|
|
||||||
NodeContent: true,
|
|
||||||
SlotConnectionDot: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.resetAllMocks()
|
||||||
// Reset to default mock
|
mockData.mockNodeIds = new Set()
|
||||||
vi.mocked(useNodeExecutionState).mockReturnValue({
|
mockData.mockExecuting = false
|
||||||
executing: computed(() => false),
|
|
||||||
progress: computed(() => undefined),
|
|
||||||
progressPercentage: computed(() => undefined),
|
|
||||||
progressState: computed(() => undefined as any),
|
|
||||||
executionState: computed(() => 'idle' as const)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call resize tracking composable with node ID', () => {
|
it('should call resize tracking composable with node ID', () => {
|
||||||
@@ -146,9 +150,6 @@ describe('LGraphNode', () => {
|
|||||||
}),
|
}),
|
||||||
i18n
|
i18n
|
||||||
],
|
],
|
||||||
provide: {
|
|
||||||
[SelectedNodeIdsKey as symbol]: ref(new Set())
|
|
||||||
},
|
|
||||||
stubs: {
|
stubs: {
|
||||||
NodeSlots: true,
|
NodeSlots: true,
|
||||||
NodeWidgets: true,
|
NodeWidgets: true,
|
||||||
@@ -162,24 +163,15 @@ describe('LGraphNode', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should apply selected styling when selected prop is true', () => {
|
it('should apply selected styling when selected prop is true', () => {
|
||||||
const wrapper = mountLGraphNode(
|
mockData.mockNodeIds = new Set(['test-node-123'])
|
||||||
{ nodeData: mockNodeData, selected: true },
|
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
|
||||||
new Set(['test-node-123'])
|
|
||||||
)
|
|
||||||
expect(wrapper.classes()).toContain('outline-2')
|
expect(wrapper.classes()).toContain('outline-2')
|
||||||
expect(wrapper.classes()).toContain('outline-black')
|
expect(wrapper.classes()).toContain('outline-black')
|
||||||
expect(wrapper.classes()).toContain('dark-theme:outline-white')
|
expect(wrapper.classes()).toContain('dark-theme:outline-white')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should apply executing animation when executing prop is true', () => {
|
it('should apply executing animation when executing prop is true', () => {
|
||||||
// Mock the execution state to return executing: true
|
mockData.mockExecuting = true
|
||||||
vi.mocked(useNodeExecutionState).mockReturnValue({
|
|
||||||
executing: computed(() => true),
|
|
||||||
progress: computed(() => undefined),
|
|
||||||
progressPercentage: computed(() => undefined),
|
|
||||||
progressState: computed(() => undefined as any),
|
|
||||||
executionState: computed(() => 'running' as const)
|
|
||||||
})
|
|
||||||
|
|
||||||
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
|
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
|
||||||
|
|
||||||
|
|||||||
@@ -1,98 +1,82 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, shallowRef } from 'vue'
|
||||||
|
|
||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import {
|
||||||
import type { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
type GraphNodeManager,
|
||||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
type VueNodeData,
|
||||||
|
useGraphNodeManager
|
||||||
|
} from '@/composables/graph/useGraphNodeManager'
|
||||||
|
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||||
|
import type {
|
||||||
|
LGraph,
|
||||||
|
LGraphCanvas,
|
||||||
|
LGraphNode
|
||||||
|
} from '@/lib/litegraph/src/litegraph'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
|
||||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||||
|
|
||||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
vi.mock('@/renderer/core/canvas/canvasStore', () => {
|
||||||
useCanvasStore: vi.fn()
|
const canvas: Partial<LGraphCanvas> = {
|
||||||
}))
|
|
||||||
|
|
||||||
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(),
|
select: vi.fn(),
|
||||||
deselect: vi.fn(),
|
deselect: vi.fn(),
|
||||||
deselectAll: vi.fn()
|
deselectAll: vi.fn()
|
||||||
}
|
}
|
||||||
}
|
const updateSelectedItems = vi.fn()
|
||||||
|
|
||||||
function createMockNode(): Pick<LGraphNode, 'id' | 'selected' | 'flags'> {
|
|
||||||
return {
|
return {
|
||||||
|
useCanvasStore: vi.fn(() => ({
|
||||||
|
canvas: canvas as LGraphCanvas,
|
||||||
|
updateSelectedItems,
|
||||||
|
selectedItems: []
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
|
||||||
|
useCanvasInteractions: vi.fn(() => ({
|
||||||
|
shouldHandleNodePointerEvents: computed(() => true) // Default to allowing pointer events
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/renderer/core/layout/operations/layoutMutations', () => {
|
||||||
|
const setSource = vi.fn()
|
||||||
|
const bringNodeToFront = vi.fn()
|
||||||
|
return {
|
||||||
|
useLayoutMutations: vi.fn(() => ({
|
||||||
|
setSource,
|
||||||
|
bringNodeToFront
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('@/composables/graph/useGraphNodeManager', () => {
|
||||||
|
const mockNode = {
|
||||||
id: 'node-1',
|
id: 'node-1',
|
||||||
selected: false,
|
selected: false,
|
||||||
flags: { pinned: false }
|
flags: { pinned: false }
|
||||||
}
|
}
|
||||||
}
|
const nodeManager = shallowRef({
|
||||||
|
getNode: vi.fn(() => mockNode as Partial<LGraphNode> as LGraphNode)
|
||||||
function createMockNodeManager(
|
} as Partial<GraphNodeManager> as GraphNodeManager)
|
||||||
node: Pick<LGraphNode, 'id' | 'selected' | 'flags'>
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
getNode: vi.fn().mockReturnValue(node) as ReturnType<
|
useGraphNodeManager: vi.fn(() => nodeManager)
|
||||||
typeof useGraphNodeManager
|
|
||||||
>['getNode']
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
function createMockCanvasStore(
|
vi.mock('@/composables/graph/useVueNodeLifecycle', () => {
|
||||||
canvas: Pick<LGraphCanvas, 'select' | 'deselect' | 'deselectAll'>
|
const nodeManager = useGraphNodeManager(undefined as unknown as LGraph)
|
||||||
): Pick<
|
|
||||||
ReturnType<typeof useCanvasStore>,
|
|
||||||
'canvas' | 'selectedItems' | 'updateSelectedItems'
|
|
||||||
> {
|
|
||||||
return {
|
return {
|
||||||
canvas: canvas as LGraphCanvas,
|
useVueNodeLifecycle: vi.fn(() => ({
|
||||||
selectedItems: [],
|
nodeManager
|
||||||
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', () => {
|
describe('useNodeEventHandlers', () => {
|
||||||
let mockCanvas: ReturnType<typeof createMockCanvas>
|
const { nodeManager: mockNodeManager } = useVueNodeLifecycle()
|
||||||
let mockNode: ReturnType<typeof createMockNode>
|
|
||||||
let mockNodeManager: ReturnType<typeof createMockNodeManager>
|
const mockNode = mockNodeManager.value!.getNode('fake_id')
|
||||||
let mockCanvasStore: ReturnType<typeof createMockCanvasStore>
|
const mockLayoutMutations = useLayoutMutations()
|
||||||
let mockLayoutMutations: ReturnType<typeof createMockLayoutMutations>
|
|
||||||
let mockCanvasInteractions: ReturnType<typeof createMockCanvasInteractions>
|
|
||||||
|
|
||||||
const testNodeData: VueNodeData = {
|
const testNodeData: VueNodeData = {
|
||||||
id: 'node-1',
|
id: 'node-1',
|
||||||
@@ -104,28 +88,13 @@ describe('useNodeEventHandlers', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockNode = createMockNode()
|
vi.restoreAllMocks()
|
||||||
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', () => {
|
describe('handleNodeSelect', () => {
|
||||||
it('should select single node on regular click', () => {
|
it('should select single node on regular click', () => {
|
||||||
const nodeManager = ref(mockNodeManager)
|
const { handleNodeSelect } = useNodeEventHandlers()
|
||||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
const { canvas, updateSelectedItems } = useCanvasStore()
|
||||||
|
|
||||||
const event = new PointerEvent('pointerdown', {
|
const event = new PointerEvent('pointerdown', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -135,17 +104,17 @@ describe('useNodeEventHandlers', () => {
|
|||||||
|
|
||||||
handleNodeSelect(event, testNodeData, false)
|
handleNodeSelect(event, testNodeData, false)
|
||||||
|
|
||||||
expect(mockCanvas.deselectAll).toHaveBeenCalledOnce()
|
expect(canvas?.deselectAll).toHaveBeenCalledOnce()
|
||||||
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
|
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
|
||||||
expect(mockCanvasStore.updateSelectedItems).toHaveBeenCalledOnce()
|
expect(updateSelectedItems).toHaveBeenCalledOnce()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should toggle selection on ctrl+click', () => {
|
it('should toggle selection on ctrl+click', () => {
|
||||||
const nodeManager = ref(mockNodeManager)
|
const { handleNodeSelect } = useNodeEventHandlers()
|
||||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
const { canvas } = useCanvasStore()
|
||||||
|
|
||||||
// Test selecting unselected node with ctrl
|
// Test selecting unselected node with ctrl
|
||||||
mockNode.selected = false
|
mockNode!.selected = false
|
||||||
|
|
||||||
const ctrlClickEvent = new PointerEvent('pointerdown', {
|
const ctrlClickEvent = new PointerEvent('pointerdown', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -155,16 +124,16 @@ describe('useNodeEventHandlers', () => {
|
|||||||
|
|
||||||
handleNodeSelect(ctrlClickEvent, testNodeData, false)
|
handleNodeSelect(ctrlClickEvent, testNodeData, false)
|
||||||
|
|
||||||
expect(mockCanvas.deselectAll).not.toHaveBeenCalled()
|
expect(canvas?.deselectAll).not.toHaveBeenCalled()
|
||||||
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
|
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should deselect on ctrl+click of selected node', () => {
|
it('should deselect on ctrl+click of selected node', () => {
|
||||||
const nodeManager = ref(mockNodeManager)
|
const { handleNodeSelect } = useNodeEventHandlers()
|
||||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
const { canvas } = useCanvasStore()
|
||||||
|
|
||||||
// Test deselecting selected node with ctrl
|
// Test deselecting selected node with ctrl
|
||||||
mockNode.selected = true
|
mockNode!.selected = true
|
||||||
|
|
||||||
const ctrlClickEvent = new PointerEvent('pointerdown', {
|
const ctrlClickEvent = new PointerEvent('pointerdown', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -174,15 +143,15 @@ describe('useNodeEventHandlers', () => {
|
|||||||
|
|
||||||
handleNodeSelect(ctrlClickEvent, testNodeData, false)
|
handleNodeSelect(ctrlClickEvent, testNodeData, false)
|
||||||
|
|
||||||
expect(mockCanvas.deselect).toHaveBeenCalledWith(mockNode)
|
expect(canvas?.deselect).toHaveBeenCalledWith(mockNode)
|
||||||
expect(mockCanvas.select).not.toHaveBeenCalled()
|
expect(canvas?.select).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle meta key (Cmd) on Mac', () => {
|
it('should handle meta key (Cmd) on Mac', () => {
|
||||||
const nodeManager = ref(mockNodeManager)
|
const { handleNodeSelect } = useNodeEventHandlers()
|
||||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
const { canvas } = useCanvasStore()
|
||||||
|
|
||||||
mockNode.selected = false
|
mockNode!.selected = false
|
||||||
|
|
||||||
const metaClickEvent = new PointerEvent('pointerdown', {
|
const metaClickEvent = new PointerEvent('pointerdown', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -192,15 +161,14 @@ describe('useNodeEventHandlers', () => {
|
|||||||
|
|
||||||
handleNodeSelect(metaClickEvent, testNodeData, false)
|
handleNodeSelect(metaClickEvent, testNodeData, false)
|
||||||
|
|
||||||
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
|
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
|
||||||
expect(mockCanvas.deselectAll).not.toHaveBeenCalled()
|
expect(canvas?.deselectAll).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should bring node to front when not pinned', () => {
|
it('should bring node to front when not pinned', () => {
|
||||||
const nodeManager = ref(mockNodeManager)
|
const { handleNodeSelect } = useNodeEventHandlers()
|
||||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
|
||||||
|
|
||||||
mockNode.flags.pinned = false
|
mockNode!.flags.pinned = false
|
||||||
|
|
||||||
const event = new PointerEvent('pointerdown')
|
const event = new PointerEvent('pointerdown')
|
||||||
handleNodeSelect(event, testNodeData, false)
|
handleNodeSelect(event, testNodeData, false)
|
||||||
@@ -211,49 +179,14 @@ describe('useNodeEventHandlers', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not bring pinned node to front', () => {
|
it('should not bring pinned node to front', () => {
|
||||||
const nodeManager = ref(mockNodeManager)
|
const { handleNodeSelect } = useNodeEventHandlers()
|
||||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
|
||||||
|
|
||||||
mockNode.flags.pinned = true
|
mockNode!.flags.pinned = true
|
||||||
|
|
||||||
const event = new PointerEvent('pointerdown')
|
const event = new PointerEvent('pointerdown')
|
||||||
handleNodeSelect(event, testNodeData, false)
|
handleNodeSelect(event, testNodeData, false)
|
||||||
|
|
||||||
expect(mockLayoutMutations.bringNodeToFront).not.toHaveBeenCalled()
|
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()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,14 +29,15 @@
|
|||||||
"rootDir": "./"
|
"rootDir": "./"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*",
|
".storybook/**/*",
|
||||||
|
"eslint.config.ts",
|
||||||
|
"global.d.ts",
|
||||||
|
"knip.config.ts",
|
||||||
"src/**/*.vue",
|
"src/**/*.vue",
|
||||||
|
"src/**/*",
|
||||||
"src/types/**/*.d.ts",
|
"src/types/**/*.d.ts",
|
||||||
"tests-ui/**/*",
|
"tests-ui/**/*",
|
||||||
"global.d.ts",
|
|
||||||
"eslint.config.ts",
|
|
||||||
"vite.config.mts",
|
"vite.config.mts",
|
||||||
"knip.config.ts",
|
"vitest.config.ts",
|
||||||
".storybook/**/*"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ export default defineConfig({
|
|||||||
ignored: [
|
ignored: [
|
||||||
'**/coverage/**',
|
'**/coverage/**',
|
||||||
'**/playwright-report/**',
|
'**/playwright-report/**',
|
||||||
'**/*.{test,spec}.ts'
|
'**/*.{test,spec}.ts',
|
||||||
|
'*.config.{ts,mts}'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export default defineConfig({
|
|||||||
'**/.{idea,git,cache,output,temp}/**',
|
'**/.{idea,git,cache,output,temp}/**',
|
||||||
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*',
|
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*',
|
||||||
'src/lib/litegraph/test/**'
|
'src/lib/litegraph/test/**'
|
||||||
]
|
],
|
||||||
|
silent: 'passed-only'
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Reference in New Issue
Block a user