Files
ComfyUI_frontend/src/core/graph/subgraph/resolveSubgraphInputTarget.test.ts
Alexander Brown fe489ec87c [backport core/1.42] test: subgraph integration contracts and expanded Playwright coverage (#10327)
Backport of #10123, #9967, and #9972 to `core/1.42`

Includes three cherry-picks in dependency order:
1. #9972 — `fix: resolve all lint warnings` (clean)
2. #9967 — `test: harden subgraph test coverage and remove low-value
tests` (clean)
3. #10123 — `test: subgraph integration contracts and expanded
Playwright coverage` (1 conflict, auto-resolved by rerere from #10326)

See #10326 for core/1.41 backport with detailed conflict resolution
notes.

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2026-03-19 18:24:31 -07:00

251 lines
8.0 KiB
TypeScript

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()
})
test('resolves widget and non-widget inputs on the same nested SubgraphNode independently', () => {
const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
'width',
'audio'
])
const innerSubgraph = createTestSubgraph({
inputs: [
{ name: 'width', type: '*' },
{ name: 'audio', type: '*' }
]
})
const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, {
id: 820
})
outerSubgraph.add(innerSubgraphNode)
const widthInput = innerSubgraphNode.addInput('width', '*')
innerSubgraphNode.addWidget('number', 'width', 0, () => undefined)
widthInput.widget = { name: 'width' }
const audioInput = innerSubgraphNode.addInput('audio', '*')
const widthSlot = outerSubgraph.inputNode.slots.find(
(slot) => slot.name === 'width'
)!
const audioSlot = outerSubgraph.inputNode.slots.find(
(slot) => slot.name === 'audio'
)!
widthSlot.connect(widthInput, innerSubgraphNode)
audioSlot.connect(audioInput, innerSubgraphNode)
expect(
resolveSubgraphInputTarget(outerSubgraphNode, 'width')
).toMatchObject({
nodeId: '820',
widgetName: 'width'
})
expect(
resolveSubgraphInputTarget(outerSubgraphNode, 'audio')
).toBeUndefined()
})
test('three-level nesting returns immediate child target, not deepest', () => {
// outer → middle → inner (concrete)
const innerSubgraph = createTestSubgraph({
inputs: [{ name: 'seed', type: '*' }]
})
const concreteNode = new LGraphNode('ConcreteNode')
concreteNode.id = 900
const concreteInput = concreteNode.addInput('seed_input', '*')
concreteNode.addWidget('number', 'seed', 0, () => undefined)
concreteInput.widget = { name: 'seed' }
innerSubgraph.add(concreteNode)
innerSubgraph.inputNode.slots[0].connect(concreteInput, concreteNode)
const middleSubgraph = createTestSubgraph({
inputs: [{ name: 'seed', type: '*' }]
})
const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, {
id: 901
})
middleSubgraph.add(innerSubgraphNode)
const middleInput = innerSubgraphNode.addInput('seed', '*')
innerSubgraphNode.addWidget('number', 'seed', 0, () => undefined)
middleInput.widget = { name: 'seed' }
middleSubgraph.inputNode.slots[0].connect(middleInput, innerSubgraphNode)
const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
'seed'
])
const middleSubgraphNode = createTestSubgraphNode(middleSubgraph, {
id: 902
})
outerSubgraph.add(middleSubgraphNode)
const outerInput = middleSubgraphNode.addInput('seed', '*')
middleSubgraphNode.addWidget('number', 'seed', 0, () => undefined)
outerInput.widget = { name: 'seed' }
outerSubgraph.inputNode.slots[0].connect(outerInput, middleSubgraphNode)
const result = resolveSubgraphInputTarget(outerSubgraphNode, 'seed')
// Should return the immediate child (middle), not the deepest (concrete)
expect(result).toMatchObject({
nodeId: '902',
widgetName: 'seed'
})
})
})