mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
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>
251 lines
8.0 KiB
TypeScript
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'
|
|
})
|
|
})
|
|
})
|