mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
[backport core/1.42] fix: resolve nodes in subgraphs for image copy/paste and display (#10184)
Backport of #10009 to `core/1.42` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10184-backport-core-1-42-fix-resolve-nodes-in-subgraphs-for-image-copy-paste-and-display-3266d73d365081fca061f93b717adb3e) by [Unito](https://www.unito.io) Co-authored-by: Christian Byrne <cbyrne@comfy.org>
This commit is contained in:
@@ -80,7 +80,7 @@ import { useLoad3d } from '@/composables/useLoad3d'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
@@ -101,7 +101,7 @@ if (isComponentWidget(props.widget)) {
|
||||
node.value = props.widget.node
|
||||
} else if (props.nodeId) {
|
||||
onMounted(() => {
|
||||
node.value = app.rootGraph?.getNodeById(props.nodeId!) || null
|
||||
node.value = resolveNode(props.nodeId!) ?? null
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
|
||||
type ResizeDirection =
|
||||
| 'top'
|
||||
@@ -558,10 +558,7 @@ export function useImageCrop(nodeId: NodeId, options: UseImageCropOptions) {
|
||||
|
||||
const initialize = () => {
|
||||
if (nodeId != null) {
|
||||
node.value =
|
||||
app.canvas?.graph?.getNodeById(nodeId) ||
|
||||
app.rootGraph?.getNodeById(nodeId) ||
|
||||
null
|
||||
node.value = resolveNode(nodeId) ?? null
|
||||
}
|
||||
|
||||
updateImageUrl()
|
||||
|
||||
@@ -131,8 +131,8 @@ import { downloadFile } from '@/base/common/downloadUtil'
|
||||
import Skeleton from '@/components/ui/skeleton/Skeleton.vue'
|
||||
import { useMaskEditor } from '@/composables/maskeditor/useMaskEditor'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
interface ImagePreviewProps {
|
||||
@@ -227,7 +227,7 @@ const handleImageError = () => {
|
||||
|
||||
const handleEditMask = () => {
|
||||
if (!props.nodeId) return
|
||||
const node = app.rootGraph?.getNodeById(Number(props.nodeId))
|
||||
const node = resolveNode(Number(props.nodeId))
|
||||
if (!node) return
|
||||
maskEditor.openMaskEditor(node)
|
||||
}
|
||||
@@ -246,7 +246,7 @@ const handleDownload = () => {
|
||||
|
||||
const handleRemove = () => {
|
||||
if (!props.nodeId) return
|
||||
const node = app.rootGraph?.getNodeById(Number(props.nodeId))
|
||||
const node = resolveNode(Number(props.nodeId))
|
||||
nodeOutputStore.removeNodeOutputs(props.nodeId)
|
||||
if (node) {
|
||||
node.imgs = undefined
|
||||
|
||||
@@ -193,8 +193,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
import { useMaskEditor } from '@/composables/maskeditor/useMaskEditor'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -374,7 +374,7 @@ function selectFromGrid(index: number) {
|
||||
|
||||
function handleEditMask() {
|
||||
if (!nodeId) return
|
||||
const node = app.rootGraph?.getNodeById(Number(nodeId))
|
||||
const node = resolveNode(Number(nodeId))
|
||||
if (!node) return
|
||||
maskEditor.openMaskEditor(node)
|
||||
}
|
||||
@@ -395,7 +395,7 @@ function handleDownload() {
|
||||
|
||||
function handleRemove() {
|
||||
if (!nodeId) return
|
||||
const node = app.rootGraph?.getNodeById(Number(nodeId))
|
||||
const node = resolveNode(Number(nodeId))
|
||||
nodeOutputStore.removeNodeOutputs(nodeId)
|
||||
if (node) {
|
||||
node.imgs = undefined
|
||||
|
||||
@@ -9,9 +9,12 @@ import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import * as litegraphUtil from '@/utils/litegraphUtil'
|
||||
|
||||
const mockResolveNode = vi.fn()
|
||||
|
||||
vi.mock('@/utils/litegraphUtil', () => ({
|
||||
isAnimatedOutput: vi.fn(),
|
||||
isVideoNode: vi.fn()
|
||||
isVideoNode: vi.fn(),
|
||||
resolveNode: (...args: unknown[]) => mockResolveNode(...args)
|
||||
}))
|
||||
|
||||
const mockGetNodeById = vi.fn()
|
||||
@@ -337,7 +340,7 @@ describe('nodeOutputStore syncLegacyNodeImgs', () => {
|
||||
const mockNode = createMockNode({ id: 1 })
|
||||
const mockImg = document.createElement('img')
|
||||
|
||||
mockGetNodeById.mockReturnValue(mockNode)
|
||||
mockResolveNode.mockReturnValue(mockNode)
|
||||
|
||||
store.syncLegacyNodeImgs(1, mockImg, 0)
|
||||
|
||||
@@ -351,7 +354,7 @@ describe('nodeOutputStore syncLegacyNodeImgs', () => {
|
||||
const mockNode = createMockNode({ id: 1 })
|
||||
const mockImg = document.createElement('img')
|
||||
|
||||
mockGetNodeById.mockReturnValue(mockNode)
|
||||
mockResolveNode.mockReturnValue(mockNode)
|
||||
|
||||
store.syncLegacyNodeImgs(1, mockImg, 0)
|
||||
|
||||
@@ -365,7 +368,7 @@ describe('nodeOutputStore syncLegacyNodeImgs', () => {
|
||||
const mockNode = createMockNode({ id: 42 })
|
||||
const mockImg = document.createElement('img')
|
||||
|
||||
mockGetNodeById.mockReturnValue(mockNode)
|
||||
mockResolveNode.mockReturnValue(mockNode)
|
||||
|
||||
store.syncLegacyNodeImgs(42, mockImg, 3)
|
||||
|
||||
@@ -379,11 +382,11 @@ describe('nodeOutputStore syncLegacyNodeImgs', () => {
|
||||
const mockNode = createMockNode({ id: 123 })
|
||||
const mockImg = document.createElement('img')
|
||||
|
||||
mockGetNodeById.mockReturnValue(mockNode)
|
||||
mockResolveNode.mockReturnValue(mockNode)
|
||||
|
||||
store.syncLegacyNodeImgs('123', mockImg, 0)
|
||||
|
||||
expect(mockGetNodeById).toHaveBeenCalledWith(123)
|
||||
expect(mockResolveNode).toHaveBeenCalledWith(123)
|
||||
expect(mockNode.imgs).toEqual([mockImg])
|
||||
})
|
||||
|
||||
@@ -392,7 +395,7 @@ describe('nodeOutputStore syncLegacyNodeImgs', () => {
|
||||
const store = useNodeOutputStore()
|
||||
const mockImg = document.createElement('img')
|
||||
|
||||
mockGetNodeById.mockReturnValue(undefined)
|
||||
mockResolveNode.mockReturnValue(undefined)
|
||||
|
||||
expect(() => store.syncLegacyNodeImgs(999, mockImg, 0)).not.toThrow()
|
||||
})
|
||||
@@ -403,10 +406,27 @@ describe('nodeOutputStore syncLegacyNodeImgs', () => {
|
||||
const mockNode = createMockNode({ id: 1 })
|
||||
const mockImg = document.createElement('img')
|
||||
|
||||
mockGetNodeById.mockReturnValue(mockNode)
|
||||
mockResolveNode.mockReturnValue(mockNode)
|
||||
|
||||
store.syncLegacyNodeImgs(1, mockImg)
|
||||
|
||||
expect(mockNode.imageIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('should sync node.imgs when node is inside a subgraph', () => {
|
||||
LiteGraph.vueNodesMode = true
|
||||
const store = useNodeOutputStore()
|
||||
const mockNode = createMockNode({ id: 5 })
|
||||
const mockImg = document.createElement('img')
|
||||
|
||||
// Node NOT in root graph (returns null)
|
||||
mockGetNodeById.mockReturnValue(null)
|
||||
// But found by resolveNode (in a subgraph)
|
||||
mockResolveNode.mockReturnValue(mockNode)
|
||||
|
||||
store.syncLegacyNodeImgs(5, mockImg, 0)
|
||||
|
||||
expect(mockNode.imgs).toEqual([mockImg])
|
||||
expect(mockNode.imageIndex).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,7 +15,11 @@ import { app } from '@/scripts/app'
|
||||
import { clone } from '@/scripts/utils'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { parseFilePath } from '@/utils/formatUtil'
|
||||
import { isAnimatedOutput, isVideoNode } from '@/utils/litegraphUtil'
|
||||
import {
|
||||
isAnimatedOutput,
|
||||
isVideoNode,
|
||||
resolveNode
|
||||
} from '@/utils/litegraphUtil'
|
||||
import {
|
||||
releaseSharedObjectUrl,
|
||||
retainSharedObjectUrl
|
||||
@@ -464,7 +468,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
) {
|
||||
if (!LiteGraph.vueNodesMode) return
|
||||
|
||||
const node = app.rootGraph?.getNodeById(Number(nodeId))
|
||||
const node = resolveNode(Number(nodeId))
|
||||
if (!node) return
|
||||
|
||||
node.imgs = [element]
|
||||
|
||||
@@ -1,442 +1,70 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
LGraph,
|
||||
LGraphCanvas,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { IWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
|
||||
import {
|
||||
compressWidgetInputSlots,
|
||||
createNode,
|
||||
isAnimatedOutput,
|
||||
isVideoOutput,
|
||||
migrateWidgetsValues,
|
||||
resolveNode
|
||||
} from '@/utils/litegraphUtil'
|
||||
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { createTestSubgraph } from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
|
||||
vi.mock('@/lib/litegraph/src/litegraph', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
LiteGraph: {
|
||||
createNode: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: { rootGraph: null }
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/updates/common/toastStore', () => ({
|
||||
useToastStore: vi.fn(() => ({
|
||||
addAlert: vi.fn(),
|
||||
add: vi.fn(),
|
||||
remove: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: vi.fn((key: string) => key)
|
||||
}))
|
||||
|
||||
function createMockCanvas(overrides: Partial<LGraphCanvas> = {}): LGraphCanvas {
|
||||
const mockGraph = {
|
||||
add: vi.fn((node) => node),
|
||||
change: vi.fn()
|
||||
} as Partial<LGraph> as LGraph
|
||||
const mockCanvas: Partial<LGraphCanvas> = {
|
||||
graph_mouse: [100, 200],
|
||||
graph: mockGraph,
|
||||
...overrides
|
||||
}
|
||||
return mockCanvas as LGraphCanvas
|
||||
}
|
||||
|
||||
describe('createNode', () => {
|
||||
beforeEach(vi.clearAllMocks)
|
||||
|
||||
it('should create a node successfully', async () => {
|
||||
const mockNode = { pos: [0, 0] }
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode)
|
||||
|
||||
const mockCanvas = createMockCanvas()
|
||||
const result = await createNode(mockCanvas, 'LoadImage')
|
||||
|
||||
expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage')
|
||||
expect(mockNode.pos).toEqual([100, 200])
|
||||
expect(mockCanvas.graph!.add).toHaveBeenCalledWith(mockNode)
|
||||
expect(mockCanvas.graph!.change).toHaveBeenCalled()
|
||||
expect(result).toBe(mockNode)
|
||||
})
|
||||
|
||||
it('should return null when name is empty', async () => {
|
||||
const mockCanvas = createMockCanvas()
|
||||
const result = await createNode(mockCanvas, '')
|
||||
|
||||
expect(LiteGraph.createNode).not.toHaveBeenCalled()
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle graph being null', async () => {
|
||||
const mockNode = { pos: [0, 0] }
|
||||
const mockCanvas = createMockCanvas({ graph: null })
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode)
|
||||
|
||||
const result = await createNode(mockCanvas, 'LoadImage')
|
||||
|
||||
expect(mockNode.pos).toEqual([0, 0])
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
it('should set position based on canvas graph_mouse', async () => {
|
||||
const mockCanvas = createMockCanvas({ graph_mouse: [250, 350] })
|
||||
const mockNode = { pos: [0, 0] }
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(mockNode as LGraphNode)
|
||||
|
||||
await createNode(mockCanvas, 'LoadAudio')
|
||||
|
||||
expect(mockNode.pos).toEqual([250, 350])
|
||||
})
|
||||
})
|
||||
|
||||
describe('migrateWidgetsValues', () => {
|
||||
it('should remove widget values for forceInput inputs', () => {
|
||||
const inputDefs: Record<string, InputSpec> = {
|
||||
normalInput: {
|
||||
type: 'INT',
|
||||
name: 'normalInput'
|
||||
},
|
||||
forceInputField: {
|
||||
type: 'STRING',
|
||||
name: 'forceInputField',
|
||||
forceInput: true
|
||||
},
|
||||
anotherNormal: {
|
||||
type: 'FLOAT',
|
||||
name: 'anotherNormal'
|
||||
}
|
||||
}
|
||||
|
||||
const widgets = [
|
||||
{ name: 'normalInput', type: 'number' },
|
||||
{ name: 'anotherNormal', type: 'number' }
|
||||
] as Partial<IWidget>[] as IWidget[]
|
||||
|
||||
const widgetValues = [42, 'dummy value', 3.14]
|
||||
|
||||
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
|
||||
expect(result).toEqual([42, 3.14])
|
||||
})
|
||||
|
||||
it('should return original values if lengths do not match', () => {
|
||||
const inputDefs: Record<string, InputSpec> = {
|
||||
input1: {
|
||||
type: 'INT',
|
||||
name: 'input1',
|
||||
forceInput: true
|
||||
}
|
||||
}
|
||||
|
||||
const widgets: IWidget[] = []
|
||||
const widgetValues = [42, 'extra value']
|
||||
|
||||
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
|
||||
expect(result).toEqual(widgetValues)
|
||||
})
|
||||
|
||||
it('should handle empty widgets and values', () => {
|
||||
const inputDefs: Record<string, InputSpec> = {}
|
||||
const widgets: IWidget[] = []
|
||||
const widgetValues: unknown[] = []
|
||||
|
||||
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should preserve order of non-forceInput widget values', () => {
|
||||
const inputDefs: Record<string, InputSpec> = {
|
||||
first: {
|
||||
type: 'INT',
|
||||
name: 'first'
|
||||
},
|
||||
forced: {
|
||||
type: 'STRING',
|
||||
name: 'forced',
|
||||
forceInput: true
|
||||
},
|
||||
last: {
|
||||
type: 'FLOAT',
|
||||
name: 'last'
|
||||
}
|
||||
}
|
||||
|
||||
const widgets = [
|
||||
{ name: 'first', type: 'number' },
|
||||
{ name: 'last', type: 'number' }
|
||||
] as Partial<IWidget>[] as IWidget[]
|
||||
|
||||
const widgetValues = ['first value', 'dummy', 'last value']
|
||||
|
||||
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
|
||||
expect(result).toEqual(['first value', 'last value'])
|
||||
})
|
||||
it('should correctly handle seed with unexpected value', () => {
|
||||
const inputDefs: Record<string, InputSpec> = {
|
||||
normalInput: {
|
||||
type: 'INT',
|
||||
name: 'normalInput',
|
||||
control_after_generate: true
|
||||
},
|
||||
forceInputField: {
|
||||
type: 'STRING',
|
||||
name: 'forceInputField',
|
||||
forceInput: true
|
||||
}
|
||||
}
|
||||
|
||||
const widgets = [
|
||||
{ name: 'normalInput', type: 'number' },
|
||||
{ name: 'control_after_generate', type: 'string' }
|
||||
] as Partial<IWidget>[] as IWidget[]
|
||||
|
||||
const widgetValues = [42, 'fixed', 'unexpected widget value']
|
||||
|
||||
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
|
||||
expect(result).toEqual([42, 'fixed'])
|
||||
})
|
||||
})
|
||||
|
||||
function createOutput(
|
||||
overrides: Partial<ExecutedWsMessage['output']> = {}
|
||||
): ExecutedWsMessage['output'] {
|
||||
return { ...overrides }
|
||||
}
|
||||
|
||||
describe('isAnimatedOutput', () => {
|
||||
it('returns false for undefined output', () => {
|
||||
expect(isAnimatedOutput(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when animated array is missing', () => {
|
||||
expect(isAnimatedOutput(createOutput())).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when all animated values are false', () => {
|
||||
expect(isAnimatedOutput(createOutput({ animated: [false, false] }))).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('returns true when any animated value is true', () => {
|
||||
expect(isAnimatedOutput(createOutput({ animated: [false, true] }))).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isVideoOutput', () => {
|
||||
it('returns false for non-animated output', () => {
|
||||
expect(
|
||||
isVideoOutput(
|
||||
createOutput({
|
||||
animated: [false],
|
||||
images: [{ filename: 'video.webm' }]
|
||||
})
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for animated webp output', () => {
|
||||
expect(
|
||||
isVideoOutput(
|
||||
createOutput({
|
||||
animated: [true],
|
||||
images: [{ filename: 'anim.webp' }]
|
||||
})
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for animated png output', () => {
|
||||
expect(
|
||||
isVideoOutput(
|
||||
createOutput({
|
||||
animated: [true],
|
||||
images: [{ filename: 'anim.png' }]
|
||||
})
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true for animated webm output', () => {
|
||||
expect(
|
||||
isVideoOutput(
|
||||
createOutput({
|
||||
animated: [true],
|
||||
images: [{ filename: 'output.webm' }]
|
||||
})
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true for animated mp4 output', () => {
|
||||
expect(
|
||||
isVideoOutput(
|
||||
createOutput({
|
||||
animated: [true],
|
||||
images: [{ filename: 'output.mp4' }]
|
||||
})
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true for animated output with no images array', () => {
|
||||
expect(isVideoOutput(createOutput({ animated: [true] }))).toBe(true)
|
||||
})
|
||||
|
||||
it('does not false-positive on filenames containing webp as substring', () => {
|
||||
expect(
|
||||
isVideoOutput(
|
||||
createOutput({
|
||||
animated: [true],
|
||||
images: [{ filename: 'my_webp_file.mp4' }]
|
||||
})
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('compressWidgetInputSlots', () => {
|
||||
it('should remove unconnected widget input slots', () => {
|
||||
// Using partial mock - only including properties needed for test
|
||||
const graph = {
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'foo',
|
||||
pos: [0, 0],
|
||||
size: [100, 100],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{ widget: { name: 'foo' }, link: null, type: 'INT', name: 'foo' },
|
||||
{ widget: { name: 'bar' }, link: 2, type: 'INT', name: 'bar' },
|
||||
{ widget: { name: 'baz' }, link: null, type: 'INT', name: 'baz' }
|
||||
],
|
||||
outputs: []
|
||||
}
|
||||
],
|
||||
links: [[2, 1, 0, 1, 0, 'INT']]
|
||||
} as Partial<ISerialisedGraph> as ISerialisedGraph
|
||||
|
||||
compressWidgetInputSlots(graph)
|
||||
|
||||
expect(graph.nodes[0].inputs).toEqual([
|
||||
{ widget: { name: 'bar' }, link: 2, type: 'INT', name: 'bar' }
|
||||
])
|
||||
})
|
||||
|
||||
it('should update link target slots correctly', () => {
|
||||
const graph = {
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'foo',
|
||||
pos: [0, 0],
|
||||
size: [100, 100],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{ widget: { name: 'foo' }, link: null, type: 'INT', name: 'foo' },
|
||||
{ widget: { name: 'bar' }, link: 2, type: 'INT', name: 'bar' },
|
||||
{ widget: { name: 'baz' }, link: 3, type: 'INT', name: 'baz' }
|
||||
],
|
||||
outputs: []
|
||||
}
|
||||
],
|
||||
links: [
|
||||
[2, 1, 0, 1, 1, 'INT'],
|
||||
[3, 1, 0, 1, 2, 'INT']
|
||||
]
|
||||
} as Partial<ISerialisedGraph> as ISerialisedGraph
|
||||
|
||||
compressWidgetInputSlots(graph)
|
||||
|
||||
expect(graph.nodes[0].inputs).toEqual([
|
||||
{ widget: { name: 'bar' }, link: 2, type: 'INT', name: 'bar' },
|
||||
{ widget: { name: 'baz' }, link: 3, type: 'INT', name: 'baz' }
|
||||
])
|
||||
|
||||
expect(graph.links).toEqual([
|
||||
[2, 1, 0, 1, 0, 'INT'],
|
||||
[3, 1, 0, 1, 1, 'INT']
|
||||
])
|
||||
})
|
||||
|
||||
it('should handle graphs with no nodes gracefully', () => {
|
||||
// Using partial mock - only including properties needed for test
|
||||
const graph = {
|
||||
nodes: [],
|
||||
links: []
|
||||
} as Partial<ISerialisedGraph> as ISerialisedGraph
|
||||
|
||||
compressWidgetInputSlots(graph)
|
||||
|
||||
expect(graph.nodes).toEqual([])
|
||||
expect(graph.links).toEqual([])
|
||||
})
|
||||
})
|
||||
import { resolveNode } from './litegraphUtil'
|
||||
|
||||
describe('resolveNode', () => {
|
||||
function mockGraph(
|
||||
nodeList: Partial<LGraphNode>[],
|
||||
subgraphs?: Map<string, LGraph>
|
||||
) {
|
||||
const nodesById: Record<string, LGraphNode> = {}
|
||||
for (const n of nodeList) {
|
||||
nodesById[String(n.id)] = n as LGraphNode
|
||||
}
|
||||
return {
|
||||
nodes: nodeList as LGraphNode[],
|
||||
getNodeById(id: unknown) {
|
||||
return id != null ? (nodesById[String(id)] ?? null) : null
|
||||
},
|
||||
subgraphs: subgraphs ?? new Map()
|
||||
} as unknown as LGraph
|
||||
}
|
||||
|
||||
it('returns undefined when graph is nullish', () => {
|
||||
it('returns undefined when graph is null', () => {
|
||||
expect(resolveNode(1, null)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined when graph is undefined', () => {
|
||||
expect(resolveNode(1, undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('finds a node in the main graph', () => {
|
||||
const node = { id: 5 } as LGraphNode
|
||||
const graph = mockGraph([node])
|
||||
expect(resolveNode(5, graph)).toBe(node)
|
||||
it('finds a node in the root graph', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('TestNode')
|
||||
graph.add(node)
|
||||
|
||||
expect(resolveNode(node.id, graph)).toBe(node)
|
||||
})
|
||||
|
||||
it('finds a node in a subgraph', () => {
|
||||
const subNode = { id: 10 } as LGraphNode
|
||||
const subgraph = mockGraph([subNode])
|
||||
const graph = mockGraph([], new Map([['sg-1', subgraph]]))
|
||||
expect(resolveNode(10, graph)).toBe(subNode)
|
||||
})
|
||||
it('returns undefined when node does not exist anywhere', () => {
|
||||
const graph = new LGraph()
|
||||
|
||||
it('returns undefined when node is not found anywhere', () => {
|
||||
const graph = mockGraph([{ id: 1 } as LGraphNode])
|
||||
expect(resolveNode(999, graph)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('prefers main graph over subgraph', () => {
|
||||
const mainNode = { id: 1, title: 'main' } as LGraphNode
|
||||
const subNode = { id: 1, title: 'sub' } as LGraphNode
|
||||
const subgraph = mockGraph([subNode])
|
||||
const graph = mockGraph([mainNode], new Map([['sg-1', subgraph]]))
|
||||
expect(resolveNode(1, graph)).toBe(mainNode)
|
||||
it('finds a node inside a subgraph', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 1 })
|
||||
const rootGraph = subgraph.rootGraph
|
||||
rootGraph._subgraphs.set(subgraph.id, subgraph)
|
||||
const subgraphNode = subgraph._nodes[0]
|
||||
|
||||
// Node should NOT be found directly on root graph
|
||||
expect(rootGraph.getNodeById(subgraphNode.id)).toBeFalsy()
|
||||
|
||||
// But resolveNode should find it via subgraph search
|
||||
expect(resolveNode(subgraphNode.id, rootGraph)).toBe(subgraphNode)
|
||||
})
|
||||
|
||||
it('prefers root graph node over subgraph node with same id', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const rootGraph = subgraph.rootGraph
|
||||
|
||||
const rootNode = new LGraphNode('RootNode')
|
||||
rootGraph.add(rootNode)
|
||||
|
||||
// Add a different node to the subgraph
|
||||
const sgNode = new LGraphNode('SubgraphNode')
|
||||
subgraph.add(sgNode)
|
||||
|
||||
// resolveNode should return the root graph node first
|
||||
expect(resolveNode(rootNode.id, rootGraph)).toBe(rootNode)
|
||||
})
|
||||
|
||||
it('searches across multiple subgraphs', () => {
|
||||
const sg1 = createTestSubgraph({ name: 'SG1' })
|
||||
const rootGraph = sg1.rootGraph
|
||||
const sg2 = createTestSubgraph({ name: 'SG2', nodeCount: 1 })
|
||||
|
||||
// Put sg2 under the same root graph
|
||||
rootGraph._subgraphs.set(sg2.id, sg2)
|
||||
|
||||
const targetNode = sg2._nodes[0]
|
||||
expect(resolveNode(targetNode.id, rootGraph)).toBe(targetNode)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user