diff --git a/src/core/graph/subgraph/resolveSubgraphInputTarget.test.ts b/src/core/graph/subgraph/resolveSubgraphInputTarget.test.ts new file mode 100644 index 0000000000..77f8542ff5 --- /dev/null +++ b/src/core/graph/subgraph/resolveSubgraphInputTarget.test.ts @@ -0,0 +1,161 @@ +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' +import { beforeEach, describe, expect, test, vi } from 'vitest' + +import { resolveSubgraphInputTarget } from '@/core/graph/subgraph/resolveSubgraphInputTarget' +import { LGraphNode } from '@/lib/litegraph/src/litegraph' +import { + createTestSubgraph, + createTestSubgraphNode +} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers' +import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph' +import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode' + +vi.mock('@/renderer/core/canvas/canvasStore', () => ({ + useCanvasStore: () => ({}) +})) +vi.mock('@/stores/domWidgetStore', () => ({ + useDomWidgetStore: () => ({ widgetStates: new Map() }) +})) +vi.mock('@/services/litegraphService', () => ({ + useLitegraphService: () => ({ updatePreviews: () => ({}) }) +})) + +function createOuterSubgraphSetup(inputNames: string[]): { + outerSubgraph: Subgraph + outerSubgraphNode: SubgraphNode +} { + const outerSubgraph = createTestSubgraph({ + inputs: inputNames.map((name) => ({ name, type: '*' })) + }) + const outerSubgraphNode = createTestSubgraphNode(outerSubgraph, { id: 1 }) + return { outerSubgraph, outerSubgraphNode } +} + +function addLinkedNestedSubgraphNode( + outerSubgraph: Subgraph, + inputName: string, + linkedInputName: string, + options: { widget?: string } = {} +): { innerSubgraphNode: SubgraphNode } { + const innerSubgraph = createTestSubgraph({ + inputs: [{ name: linkedInputName, type: '*' }] + }) + const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, { id: 819 }) + outerSubgraph.add(innerSubgraphNode) + + const inputSlot = outerSubgraph.inputNode.slots.find( + (slot) => slot.name === inputName + ) + if (!inputSlot) throw new Error(`Missing subgraph input slot: ${inputName}`) + + const input = innerSubgraphNode.addInput(linkedInputName, '*') + if (options.widget) { + innerSubgraphNode.addWidget('number', options.widget, 0, () => undefined) + input.widget = { name: options.widget } + } + inputSlot.connect(input, innerSubgraphNode) + + if (input.link == null) { + throw new Error(`Expected link to be created for input ${linkedInputName}`) + } + + return { innerSubgraphNode } +} + +beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) + vi.clearAllMocks() +}) + +describe('resolveSubgraphInputTarget', () => { + test('returns target for widget-backed input on nested SubgraphNode', () => { + const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([ + 'width' + ]) + addLinkedNestedSubgraphNode(outerSubgraph, 'width', 'width', { + widget: 'width' + }) + + const result = resolveSubgraphInputTarget(outerSubgraphNode, 'width') + + expect(result).toMatchObject({ + nodeId: '819', + widgetName: 'width' + }) + }) + + test('returns undefined for non-widget input on nested SubgraphNode', () => { + const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([ + 'audio' + ]) + addLinkedNestedSubgraphNode(outerSubgraph, 'audio', 'audio') + + const result = resolveSubgraphInputTarget(outerSubgraphNode, 'audio') + + expect(result).toBeUndefined() + }) + + test('resolves widget inputs but not non-widget inputs on the same nested SubgraphNode', () => { + const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([ + 'width', + 'audio' + ]) + addLinkedNestedSubgraphNode(outerSubgraph, 'width', 'width', { + widget: 'width' + }) + addLinkedNestedSubgraphNode(outerSubgraph, 'audio', 'audio') + + expect( + resolveSubgraphInputTarget(outerSubgraphNode, 'width') + ).toMatchObject({ + nodeId: '819', + widgetName: 'width' + }) + expect( + resolveSubgraphInputTarget(outerSubgraphNode, 'audio') + ).toBeUndefined() + }) + + test('returns target for widget-backed input on plain interior node', () => { + const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([ + 'seed' + ]) + + const inputSlot = outerSubgraph.inputNode.slots.find( + (slot) => slot.name === 'seed' + )! + const node = new LGraphNode('Interior-seed') + node.id = 42 + const input = node.addInput('seed_input', '*') + node.addWidget('number', 'seed', 0, () => undefined) + input.widget = { name: 'seed' } + outerSubgraph.add(node) + inputSlot.connect(input, node) + + const result = resolveSubgraphInputTarget(outerSubgraphNode, 'seed') + + expect(result).toMatchObject({ + nodeId: '42', + widgetName: 'seed' + }) + }) + + test('returns undefined for non-widget input on plain interior node', () => { + const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([ + 'image' + ]) + + const inputSlot = outerSubgraph.inputNode.slots.find( + (slot) => slot.name === 'image' + )! + const node = new LGraphNode('Interior-image') + const input = node.addInput('image_input', '*') + outerSubgraph.add(node) + inputSlot.connect(input, node) + + const result = resolveSubgraphInputTarget(outerSubgraphNode, 'image') + + expect(result).toBeUndefined() + }) +}) diff --git a/src/core/graph/subgraph/resolveSubgraphInputTarget.ts b/src/core/graph/subgraph/resolveSubgraphInputTarget.ts index 339933db0e..1cdf87955a 100644 --- a/src/core/graph/subgraph/resolveSubgraphInputTarget.ts +++ b/src/core/graph/subgraph/resolveSubgraphInputTarget.ts @@ -16,6 +16,9 @@ export function resolveSubgraphInputTarget( inputName, ({ inputNode, targetInput, getTargetWidget }) => { if (inputNode.isSubgraphNode()) { + const targetWidget = getTargetWidget() + if (!targetWidget) return undefined + return { nodeId: String(inputNode.id), widgetName: targetInput.name