Merge branch 'main' into deepme987/feat/missing-node-telemetry

This commit is contained in:
Deep Mehta
2026-03-18 12:13:08 -07:00
committed by GitHub
15 changed files with 435 additions and 77 deletions

View File

@@ -29,7 +29,8 @@ export const webSocketFixture = base.extend<{
function ([data, url]) {
if (!url) {
// If no URL specified, use page URL
const u = new URL(window.location.toString())
const u = new URL(window.location.href)
u.hash = ''
u.protocol = 'ws:'
u.pathname = '/'
url = u.toString() + 'ws'

View File

@@ -767,11 +767,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
})
// Click breadcrumb to navigate back to parent graph
const homeBreadcrumb = comfyPage.page.getByRole('link', {
// In the subgraph navigation breadcrumbs, the home/top level
// breadcrumb is just the workflow name without the folder path
name: 'subgraph-with-promoted-text-widget'
})
const homeBreadcrumb = comfyPage.page.locator(
'.p-breadcrumb-list > :first-child'
)
await homeBreadcrumb.waitFor({ state: 'visible' })
await homeBreadcrumb.click()
await comfyPage.nextFrame()

View File

@@ -743,15 +743,7 @@ test.describe(
await comfyPage.nextFrame()
// Navigate back via breadcrumb
await comfyPage.page
.getByTestId(TestIds.breadcrumb.subgraph)
.waitFor({ state: 'visible', timeout: 5000 })
const homeBreadcrumb = comfyPage.page.getByRole('link', {
name: 'subgraph-with-promoted-text-widget'
})
await homeBreadcrumb.waitFor({ state: 'visible' })
await homeBreadcrumb.click()
await comfyPage.nextFrame()
await exitSubgraphToParent(comfyPage)
// Widget count should be reduced
const finalWidgetCount = await getPromotedWidgetCount(comfyPage, '11')

View File

@@ -2,9 +2,116 @@ import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
import type { ComfyPage } from '../../../fixtures/ComfyPage'
const CREATE_GROUP_HOTKEY = 'Control+g'
type NodeGroupCenteringError = {
horizontal: number
vertical: number
}
type NodeGroupCenteringErrors = {
innerGroup: NodeGroupCenteringError
outerGroup: NodeGroupCenteringError
}
const LEGACY_VUE_CENTERING_BASELINE: NodeGroupCenteringErrors = {
innerGroup: {
horizontal: 16.308832840862777,
vertical: 17.390899314547084
},
outerGroup: {
horizontal: 20.30164329441476,
vertical: 42.196324096481476
}
} as const
const CENTERING_TOLERANCE = {
innerGroup: 6,
outerGroup: 12
} as const
function expectWithinBaseline(
actual: number,
baseline: number,
tolerance: number
) {
expect(Math.abs(actual - baseline)).toBeLessThan(tolerance)
}
async function getNodeGroupCenteringErrors(
comfyPage: ComfyPage
): Promise<NodeGroupCenteringErrors> {
return comfyPage.page.evaluate(() => {
type GraphNode = {
id: number | string
pos: ReadonlyArray<number>
}
type GraphGroup = {
title: string
pos: ReadonlyArray<number>
size: ReadonlyArray<number>
}
const app = window.app!
const node = app.graph.nodes[0] as GraphNode | undefined
if (!node) {
throw new Error('Expected a node in the loaded workflow')
}
const nodeElement = document.querySelector<HTMLElement>(
`[data-node-id="${node.id}"]`
)
if (!nodeElement) {
throw new Error(`Vue node element not found for node ${node.id}`)
}
const groups = app.graph.groups as GraphGroup[]
const innerGroup = groups.find((group) => group.title === 'Inner Group')
const outerGroup = groups.find((group) => group.title === 'Outer Group')
if (!innerGroup || !outerGroup) {
throw new Error('Expected both Inner Group and Outer Group in graph')
}
const nodeRect = nodeElement.getBoundingClientRect()
const getCenteringError = (group: GraphGroup): NodeGroupCenteringError => {
const [groupStartX, groupStartY] = app.canvasPosToClientPos([
group.pos[0],
group.pos[1]
])
const [groupEndX, groupEndY] = app.canvasPosToClientPos([
group.pos[0] + group.size[0],
group.pos[1] + group.size[1]
])
const groupLeft = Math.min(groupStartX, groupEndX)
const groupRight = Math.max(groupStartX, groupEndX)
const groupTop = Math.min(groupStartY, groupEndY)
const groupBottom = Math.max(groupStartY, groupEndY)
const leftGap = nodeRect.left - groupLeft
const rightGap = groupRight - nodeRect.right
const topGap = nodeRect.top - groupTop
const bottomGap = groupBottom - nodeRect.bottom
return {
horizontal: Math.abs(leftGap - rightGap),
vertical: Math.abs(topGap - bottomGap)
}
}
return {
innerGroup: getCenteringError(innerGroup),
outerGroup: getCenteringError(outerGroup)
}
})
}
test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
@@ -74,4 +181,45 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
expect(finalOffsetY).toBeCloseTo(initialOffsetY, 0)
}).toPass({ timeout: 5000 })
})
test('should keep groups aligned after loading legacy Vue workflows', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('groups/nested-groups-1-inner-node')
await comfyPage.vueNodes.waitForNodes(1)
const workflowRendererVersion = await comfyPage.page.evaluate(() => {
const extra = window.app!.graph.extra as
| { workflowRendererVersion?: string }
| undefined
return extra?.workflowRendererVersion
})
expect(workflowRendererVersion).toMatch(/^Vue/)
await expect(async () => {
const centeringErrors = await getNodeGroupCenteringErrors(comfyPage)
expectWithinBaseline(
centeringErrors.innerGroup.horizontal,
LEGACY_VUE_CENTERING_BASELINE.innerGroup.horizontal,
CENTERING_TOLERANCE.innerGroup
)
expectWithinBaseline(
centeringErrors.innerGroup.vertical,
LEGACY_VUE_CENTERING_BASELINE.innerGroup.vertical,
CENTERING_TOLERANCE.innerGroup
)
expectWithinBaseline(
centeringErrors.outerGroup.horizontal,
LEGACY_VUE_CENTERING_BASELINE.outerGroup.horizontal,
CENTERING_TOLERANCE.outerGroup
)
expectWithinBaseline(
centeringErrors.outerGroup.vertical,
LEGACY_VUE_CENTERING_BASELINE.outerGroup.vertical,
CENTERING_TOLERANCE.outerGroup
)
}).toPass({ timeout: 5000 })
})
})

View File

@@ -81,6 +81,7 @@
"@tiptap/starter-kit": "catalog:",
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"@vueuse/router": "^14.2.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-serialize": "^0.13.0",
"@xterm/xterm": "^5.5.0",

24
pnpm-lock.yaml generated
View File

@@ -461,6 +461,9 @@ importers:
'@vueuse/integrations':
specifier: 'catalog:'
version: 14.2.0(axios@1.13.5)(fuse.js@7.0.0)(vue@3.5.13(typescript@5.9.3))
'@vueuse/router':
specifier: ^14.2.0
version: 14.2.1(vue-router@4.4.3(vue@3.5.13(typescript@5.9.3)))(vue@3.5.13(typescript@5.9.3))
'@xterm/addon-fit':
specifier: ^0.10.0
version: 0.10.0(@xterm/xterm@5.5.0)
@@ -4427,6 +4430,12 @@ packages:
'@vueuse/metadata@14.2.0':
resolution: {integrity: sha512-i3axTGjU8b13FtyR4Keeama+43iD+BwX9C2TmzBVKqjSHArF03hjkp2SBZ1m72Jk2UtrX0aYCugBq2R1fhkuAQ==}
'@vueuse/router@14.2.1':
resolution: {integrity: sha512-SbZfJe+qn5bj78zNOXT4nYbnp8OIFMyAsdcJb4Y0y9vXi1TsOfglF+YIazi5DPO2lk6/ZukpN5DEQe6KrNOjMw==}
peerDependencies:
vue: ^3.5.0
vue-router: ^4.0.0 || ^5.0.0
'@vueuse/shared@12.8.2':
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
@@ -4435,6 +4444,11 @@ packages:
peerDependencies:
vue: ^3.5.0
'@vueuse/shared@14.2.1':
resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==}
peerDependencies:
vue: ^3.5.0
'@webgpu/types@0.1.66':
resolution: {integrity: sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==}
@@ -12795,6 +12809,12 @@ snapshots:
'@vueuse/metadata@14.2.0': {}
'@vueuse/router@14.2.1(vue-router@4.4.3(vue@3.5.13(typescript@5.9.3)))(vue@3.5.13(typescript@5.9.3))':
dependencies:
'@vueuse/shared': 14.2.1(vue@3.5.13(typescript@5.9.3))
vue: 3.5.13(typescript@5.9.3)
vue-router: 4.4.3(vue@3.5.13(typescript@5.9.3))
'@vueuse/shared@12.8.2(typescript@5.9.3)':
dependencies:
vue: 3.5.13(typescript@5.9.3)
@@ -12805,6 +12825,10 @@ snapshots:
dependencies:
vue: 3.5.13(typescript@5.9.3)
'@vueuse/shared@14.2.1(vue@3.5.13(typescript@5.9.3))':
dependencies:
vue: 3.5.13(typescript@5.9.3)
'@webgpu/types@0.1.66': {}
'@xstate/fsm@1.6.5': {}

View File

@@ -1,12 +1,11 @@
<template>
<a
<div
ref="wrapperRef"
v-tooltip.bottom="{
value: tooltipText,
showDelay: 512
}"
draggable="false"
href="#"
class="p-breadcrumb-item-link h-8 cursor-pointer px-2"
:class="{
'flex items-center gap-1': isActive,
@@ -23,7 +22,7 @@
<span class="p-breadcrumb-item-label px-2">{{ item.label }}</span>
<Tag v-if="item.isBlueprint" value="Blueprint" severity="primary" />
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
</a>
</div>
<Menu
v-if="isActive || isRoot"
ref="menu"

View File

@@ -0,0 +1,94 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { syncLayoutStoreNodeBoundsFromGraph } from './syncLayoutStoreFromGraph'
function createGraph(nodes: LGraphNode[]): LGraph {
return {
nodes
} as LGraph
}
function createNode(
id: number,
pos: [number, number],
size: [number, number]
): LGraphNode {
return {
id,
pos,
size
} as LGraphNode
}
describe('syncLayoutStoreNodeBoundsFromGraph', () => {
beforeEach(() => {
vi.restoreAllMocks()
LiteGraph.vueNodesMode = false
})
it('syncs node bounds to layout store when Vue nodes mode is enabled', () => {
LiteGraph.vueNodesMode = true
const batchUpdateNodeBounds = vi
.spyOn(layoutStore, 'batchUpdateNodeBounds')
.mockImplementation(() => {})
const graph = createGraph([
createNode(1, [100, 200], [320, 140]),
createNode(2, [450, 300], [225, 96])
])
syncLayoutStoreNodeBoundsFromGraph(graph)
expect(batchUpdateNodeBounds).toHaveBeenCalledWith([
{
nodeId: '1',
bounds: {
x: 100,
y: 200,
width: 320,
height: 140
}
},
{
nodeId: '2',
bounds: {
x: 450,
y: 300,
width: 225,
height: 96
}
}
])
})
it('does nothing when Vue nodes mode is disabled', () => {
const batchUpdateNodeBounds = vi
.spyOn(layoutStore, 'batchUpdateNodeBounds')
.mockImplementation(() => {})
const graph = createGraph([createNode(1, [100, 200], [320, 140])])
syncLayoutStoreNodeBoundsFromGraph(graph)
expect(batchUpdateNodeBounds).not.toHaveBeenCalled()
})
it('does nothing when graph has no nodes', () => {
LiteGraph.vueNodesMode = true
const batchUpdateNodeBounds = vi
.spyOn(layoutStore, 'batchUpdateNodeBounds')
.mockImplementation(() => {})
const graph = createGraph([])
syncLayoutStoreNodeBoundsFromGraph(graph)
expect(batchUpdateNodeBounds).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,23 @@
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { NodeBoundsUpdate } from '@/renderer/core/layout/types'
export function syncLayoutStoreNodeBoundsFromGraph(graph: LGraph): void {
if (!LiteGraph.vueNodesMode) return
const nodes = graph.nodes ?? []
if (nodes.length === 0) return
const updates: NodeBoundsUpdate[] = nodes.map((node) => ({
nodeId: String(node.id),
bounds: {
x: node.pos[0],
y: node.pos[1],
width: node.size[0],
height: node.size[1]
}
}))
layoutStore.batchUpdateNodeBounds(updates)
}

View File

@@ -204,11 +204,7 @@ useResizeObserver(outputsRef, () => {
lastScrollWidth = outputsRef.value?.scrollWidth ?? 0
})
watch(
[
() => store.activeWorkflowInProgressItems.length,
() => visibleHistory.value[0]?.id,
queueCount
],
() => visibleHistory.value[0]?.id,
() => {
const el = outputsRef.value
if (!el || el.scrollLeft === 0) {
@@ -302,59 +298,57 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
})
</script>
<template>
<div role="group" class="min-w-0 px-4 pb-4">
<div
role="group"
class="flex h-21 min-w-0 items-start justify-center px-4 py-3 pb-4"
>
<div
v-if="queueCount > 0 || hasActiveContent"
class="flex h-15 shrink-0 items-start gap-0.5"
>
<OutputHistoryActiveQueueItem
v-if="queueCount > 1"
class="mr-3"
:queue-count="queueCount"
/>
<div
v-if="mayBeActiveWorkflowPending"
:ref="selectedRef('slot:pending')"
v-bind="itemAttrs('slot:pending')"
:class="itemClass"
@click="store.select('slot:pending')"
>
<OutputPreviewItem />
</div>
<div
v-for="item in store.activeWorkflowInProgressItems"
:key="`${item.id}-${item.state}`"
:ref="selectedRef(`slot:${item.id}`)"
v-bind="itemAttrs(`slot:${item.id}`)"
:class="itemClass"
@click="store.select(`slot:${item.id}`)"
>
<OutputPreviewItem
v-if="item.state !== 'image' || !item.output"
:latent-preview="item.latentPreviewUrl"
/>
<OutputHistoryItem v-else :output="item.output" />
</div>
<div
v-if="hasActiveContent && visibleHistory.length > 0"
class="mx-4 h-12 shrink-0 border-l border-border-default"
/>
</div>
<article
ref="outputsRef"
data-testid="linear-outputs"
class="min-w-0 overflow-x-auto overflow-y-clip py-3"
class="min-w-0 overflow-x-auto overflow-y-clip"
>
<div class="mx-auto flex h-21 w-fit items-start gap-0.5">
<div
v-if="queueCount > 0 || hasActiveContent"
:class="
cn(
'sticky left-0 z-10 flex shrink-0 items-start gap-0.5',
'md:bg-comfy-menu-secondary-bg bg-comfy-menu-bg'
)
"
>
<OutputHistoryActiveQueueItem
v-if="queueCount > 1"
class="mr-3"
:queue-count="queueCount"
/>
<div
v-if="mayBeActiveWorkflowPending"
:ref="selectedRef('slot:pending')"
v-bind="itemAttrs('slot:pending')"
:class="itemClass"
@click="store.select('slot:pending')"
>
<OutputPreviewItem />
</div>
<div
v-for="item in store.activeWorkflowInProgressItems"
:key="`${item.id}-${item.state}`"
:ref="selectedRef(`slot:${item.id}`)"
v-bind="itemAttrs(`slot:${item.id}`)"
:class="itemClass"
@click="store.select(`slot:${item.id}`)"
>
<OutputPreviewItem
v-if="item.state !== 'image' || !item.output"
:latent-preview="item.latentPreviewUrl"
/>
<OutputHistoryItem v-else :output="item.output" />
</div>
<div
v-if="hasActiveContent && visibleHistory.length > 0"
class="mx-4 h-12 shrink-0 border-l border-border-default"
/>
</div>
<div class="flex h-15 w-fit items-start gap-0.5">
<template v-for="(asset, aIdx) in visibleHistory" :key="asset.id">
<div
v-if="aIdx > 0"

View File

@@ -6,6 +6,7 @@ import { shallowRef } from 'vue'
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { syncLayoutStoreNodeBoundsFromGraph } from '@/renderer/core/layout/sync/syncLayoutStoreFromGraph'
import { flushScheduledSlotLayoutSync } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { st, t } from '@/i18n'
@@ -74,6 +75,7 @@ import { useModelStore } from '@/stores/modelStore'
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
import { useNodeReplacementStore } from '@/platform/nodeReplacement/nodeReplacementStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
import { useWidgetStore } from '@/stores/widgetStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
@@ -1274,6 +1276,7 @@ export class ComfyApp {
ChangeTracker.isLoadingGraph = true
try {
let normalizedMainGraph = false
try {
// @ts-expect-error Discrepancies between zod and litegraph - in progress
this.rootGraph.configure(graphData)
@@ -1283,7 +1286,10 @@ export class ComfyApp {
this.rootGraph.extra.workflowRendererVersion
// Scale main graph
ensureCorrectLayoutScale(originalMainGraphRenderer, this.rootGraph)
normalizedMainGraph = ensureCorrectLayoutScale(
originalMainGraphRenderer,
this.rootGraph
)
// Scale all subgraphs that were loaded with the workflow
// Use original main graph renderer as fallback (not the modified one)
@@ -1361,6 +1367,10 @@ export class ComfyApp {
useExtensionService().invokeExtensions('loadedGraphNode', node)
})
if (normalizedMainGraph) {
syncLayoutStoreNodeBoundsFromGraph(this.rootGraph)
}
await useExtensionService().invokeExtensionsAsync(
'afterConfigureGraph',
missingNodeTypes
@@ -1398,6 +1408,7 @@ export class ComfyApp {
useWorkflowService().showPendingWarnings()
}
void useSubgraphNavigationStore().updateHash()
requestAnimationFrame(() => {
this.canvas.setDirty(true, true)
})

View File

@@ -47,6 +47,7 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
vi.mock('@/utils/graphTraversalUtil', () => ({
findSubgraphPathById: vi.fn()
}))
vi.mock('@vueuse/router', () => ({ useRouteHash: vi.fn() }))
describe('useSubgraphNavigationStore', () => {
beforeEach(() => {

View File

@@ -1,14 +1,17 @@
import QuickLRU from '@alloc/quick-lru'
import { useRouteHash } from '@vueuse/router'
import { defineStore } from 'pinia'
import { computed, ref, shallowRef, watch } from 'vue'
import { useRouter } from 'vue-router'
import type { DragAndScaleState } from '@/lib/litegraph/src/DragAndScale'
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'
import { findSubgraphPathById } from '@/utils/graphTraversalUtil'
import { isNonNullish } from '@/utils/typeGuardUtil'
import { isNonNullish, isSubgraph } from '@/utils/typeGuardUtil'
/**
* Stores the current subgraph navigation state; a stack representing subgraph
@@ -20,6 +23,8 @@ export const useSubgraphNavigationStore = defineStore(
() => {
const workflowStore = useWorkflowStore()
const canvasStore = useCanvasStore()
const router = useRouter()
const routeHash = useRouteHash()
/** The currently opened subgraph. */
const activeSubgraph = shallowRef<Subgraph>()
@@ -38,8 +43,6 @@ export const useSubgraphNavigationStore = defineStore(
*/
const getCurrentRootGraphId = () => {
const canvas = canvasStore.getCanvas()
if (!canvas) return 'root'
return canvas.graph?.rootGraph?.id ?? 'root'
}
@@ -158,6 +161,72 @@ export const useSubgraphNavigationStore = defineStore(
}
)
//Allow navigation with forward/back buttons
let blockHashUpdate = false
let initialLoad = true
async function navigateToHash(newHash: string) {
const root = app.rootGraph
const locatorId = newHash?.slice(1) || root.id
const canvas = canvasStore.getCanvas()
if (canvas.graph?.id === locatorId) return
const targetGraph =
(locatorId || root.id) !== root.id
? root.subgraphs.get(locatorId)
: root
if (targetGraph) return canvas.setGraph(targetGraph)
//Search all open workflows
for (const workflow of workflowStore.openWorkflows) {
const { activeState } = workflow
if (!activeState) continue
const subgraphs = activeState.definitions?.subgraphs ?? []
for (const graph of [activeState, ...subgraphs]) {
if (graph.id !== locatorId) continue
//This will trigger a navigation, which can break forward history
try {
blockHashUpdate = true
await useWorkflowService().openWorkflow(workflow)
} finally {
blockHashUpdate = false
}
const targetGraph =
app.rootGraph.id === locatorId
? app.rootGraph
: app.rootGraph.subgraphs.get(locatorId)
if (!targetGraph) {
console.error('subgraph poofed after load?')
return
}
return canvas.setGraph(targetGraph)
}
}
}
async function updateHash() {
if (blockHashUpdate) return
if (initialLoad) {
initialLoad = false
if (!routeHash.value) return
await navigateToHash(routeHash.value)
const graph = canvasStore.getCanvas().graph
if (isSubgraph(graph)) workflowStore.activeSubgraph = graph
return
}
const newId = canvasStore.getCanvas().graph?.id ?? ''
if (!routeHash.value) await router.replace('#' + app.rootGraph.id)
const currentId = routeHash.value?.slice(1)
if (!newId || newId === currentId) return
await router.push('#' + newId)
}
//update navigation hash
//NOTE: Doesn't apply on workflow load
watch(() => canvasStore.currentGraph, updateHash)
watch(routeHash, () => navigateToHash(String(routeHash.value)))
return {
activeSubgraph,
navigationStack,
@@ -165,6 +234,7 @@ export const useSubgraphNavigationStore = defineStore(
exportState,
saveViewport,
restoreViewport,
updateHash,
viewportCache
}
}

View File

@@ -46,6 +46,7 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
getCanvas: () => app.canvas
})
}))
vi.mock('@vueuse/router', () => ({ useRouteHash: vi.fn() }))
// Get reference to mock canvas
const mockCanvas = app.canvas

View File

@@ -7,10 +7,11 @@ import type { ExportedSubgraph } from '@/lib/litegraph/src/types/serialisation'
import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template'
import { api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { useLitegraphService } from '@/services/litegraphService'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
import { useLitegraphService } from '@/services/litegraphService'
import {
createTestSubgraph,
createTestSubgraphNode