mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-30 21:09:53 +00:00
 Also fixes connections to widget inputs created by a dynamic combo breaking on reload. Performs some refactoring to group the prior dynamic inputs code. See also, the overarching frontend PR: comfyanonymous/ComfyUI#10832 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6830-Add-support-for-growable-inputs-2b36d73d365081c484ebc251a10aa6dd) by [Unito](https://www.unito.io)
179 lines
5.8 KiB
TypeScript
179 lines
5.8 KiB
TypeScript
import { setActivePinia } from 'pinia'
|
|
import { createTestingPinia } from '@pinia/testing'
|
|
import { describe, expect, test } from 'vitest'
|
|
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
|
|
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
|
import { useLitegraphService } from '@/services/litegraphService'
|
|
import type { HasInitialMinSize } from '@/services/litegraphService'
|
|
|
|
setActivePinia(createTestingPinia())
|
|
type DynamicInputs = ('INT' | 'STRING' | 'IMAGE' | DynamicInputs)[][]
|
|
|
|
const { addNodeInput } = useLitegraphService()
|
|
|
|
function nextTick() {
|
|
return new Promise<void>((r) => requestAnimationFrame(() => r()))
|
|
}
|
|
|
|
function addDynamicCombo(node: LGraphNode, inputs: DynamicInputs) {
|
|
const namePrefix = `${node.widgets?.length ?? 0}`
|
|
function getSpec(
|
|
inputs: DynamicInputs,
|
|
depth: number = 0
|
|
): { key: string; inputs: object }[] {
|
|
return inputs.map((group, groupIndex) => {
|
|
const inputs = group.map((input, inputIndex) => [
|
|
`${namePrefix}.${depth}.${inputIndex}`,
|
|
Array.isArray(input)
|
|
? ['COMFY_DYNAMICCOMBO_V3', { options: getSpec(input, depth + 1) }]
|
|
: [input, {}]
|
|
])
|
|
return {
|
|
key: `${groupIndex}`,
|
|
inputs: { required: Object.fromEntries(inputs) }
|
|
}
|
|
})
|
|
}
|
|
const inputSpec: Required<InputSpec> = [
|
|
'COMFY_DYNAMICCOMBO_V3',
|
|
{ options: getSpec(inputs) }
|
|
]
|
|
addNodeInput(
|
|
node,
|
|
transformInputSpecV1ToV2(inputSpec, { name: namePrefix, isOptional: false })
|
|
)
|
|
}
|
|
function addAutogrow(node: LGraphNode, template: unknown) {
|
|
addNodeInput(
|
|
node,
|
|
transformInputSpecV1ToV2(['COMFY_AUTOGROW_V3', { template }], {
|
|
name: `${node.inputs.length}`,
|
|
isOptional: false
|
|
})
|
|
)
|
|
}
|
|
function connectInput(node: LGraphNode, inputIndex: number, graph: LGraph) {
|
|
const node2 = testNode()
|
|
node2.addOutput('out', '*')
|
|
graph.add(node2)
|
|
node2.connect(0, node, inputIndex)
|
|
}
|
|
function testNode() {
|
|
const node: LGraphNode & Partial<HasInitialMinSize> = new LGraphNode('test')
|
|
node.widgets = []
|
|
node._initialMinSize = { width: 1, height: 1 }
|
|
node.constructor.nodeData = {
|
|
name: 'testnode'
|
|
} as typeof node.constructor.nodeData
|
|
return node as LGraphNode & Required<Pick<LGraphNode, 'widgets'>>
|
|
}
|
|
|
|
describe('Dynamic Combos', () => {
|
|
test('Can add widget on selection', () => {
|
|
const node = testNode()
|
|
addDynamicCombo(node, [['INT'], ['INT', 'STRING']])
|
|
expect(node.widgets.length).toBe(2)
|
|
node.widgets[0].value = '1'
|
|
expect(node.widgets.length).toBe(3)
|
|
})
|
|
test('Can add nested widgets', () => {
|
|
const node = testNode()
|
|
addDynamicCombo(node, [['INT'], [[[], ['STRING']]]])
|
|
expect(node.widgets.length).toBe(2)
|
|
node.widgets[0].value = '1'
|
|
expect(node.widgets.length).toBe(2)
|
|
node.widgets[1].value = '1'
|
|
expect(node.widgets.length).toBe(3)
|
|
})
|
|
test('Can add input', () => {
|
|
const node = testNode()
|
|
addDynamicCombo(node, [['INT'], ['IMAGE']])
|
|
expect(node.widgets.length).toBe(2)
|
|
node.widgets[0].value = '1'
|
|
expect(node.widgets.length).toBe(1)
|
|
expect(node.inputs.length).toBe(2)
|
|
expect(node.inputs[1].type).toBe('IMAGE')
|
|
})
|
|
test('Dynamically added inputs are well ordered', () => {
|
|
const node = testNode()
|
|
addDynamicCombo(node, [['INT'], ['IMAGE']])
|
|
addDynamicCombo(node, [['INT'], ['IMAGE']])
|
|
node.widgets[2].value = '1'
|
|
node.widgets[0].value = '1'
|
|
expect(node.widgets.length).toBe(2)
|
|
expect(node.inputs.length).toBe(4)
|
|
expect(node.inputs[1].name).toBe('0.0.0.0')
|
|
expect(node.inputs[3].name).toBe('2.2.0.0')
|
|
})
|
|
})
|
|
describe('Autogrow', () => {
|
|
const inputsSpec = { required: { image: ['IMAGE', {}] } }
|
|
test('Can name by prefix', () => {
|
|
const graph = new LGraph()
|
|
const node = testNode()
|
|
graph.add(node)
|
|
addAutogrow(node, { input: inputsSpec, prefix: 'test' })
|
|
connectInput(node, 0, graph)
|
|
connectInput(node, 1, graph)
|
|
connectInput(node, 2, graph)
|
|
expect(node.inputs.length).toBe(4)
|
|
expect(node.inputs[0].name).toBe('test0')
|
|
expect(node.inputs[2].name).toBe('test2')
|
|
})
|
|
test('Can name by list of names', () => {
|
|
const graph = new LGraph()
|
|
const node = testNode()
|
|
graph.add(node)
|
|
addAutogrow(node, { input: inputsSpec, names: ['a', 'b', 'c'] })
|
|
connectInput(node, 0, graph)
|
|
connectInput(node, 1, graph)
|
|
connectInput(node, 2, graph)
|
|
expect(node.inputs.length).toBe(3)
|
|
expect(node.inputs[0].name).toBe('a')
|
|
expect(node.inputs[2].name).toBe('c')
|
|
})
|
|
test('Can add autogrow with min input count', () => {
|
|
const node = testNode()
|
|
addAutogrow(node, { min: 4, input: inputsSpec })
|
|
expect(node.inputs.length).toBe(4)
|
|
})
|
|
test('Adding connections will cause growth up to max', () => {
|
|
const graph = new LGraph()
|
|
const node = testNode()
|
|
graph.add(node)
|
|
addAutogrow(node, { min: 1, input: inputsSpec, prefix: 'test', max: 3 })
|
|
expect(node.inputs.length).toBe(1)
|
|
|
|
connectInput(node, 0, graph)
|
|
expect(node.inputs.length).toBe(2)
|
|
connectInput(node, 1, graph)
|
|
expect(node.inputs.length).toBe(3)
|
|
connectInput(node, 2, graph)
|
|
expect(node.inputs.length).toBe(3)
|
|
})
|
|
test('Removing connections decreases to min', async () => {
|
|
const graph = new LGraph()
|
|
const node = testNode()
|
|
graph.add(node)
|
|
addAutogrow(node, { min: 4, input: inputsSpec, prefix: 'test' })
|
|
connectInput(node, 3, graph)
|
|
connectInput(node, 4, graph)
|
|
connectInput(node, 5, graph)
|
|
expect(node.inputs.length).toBe(7)
|
|
|
|
node.disconnectInput(4)
|
|
await nextTick()
|
|
expect(node.inputs.length).toBe(6)
|
|
node.disconnectInput(3)
|
|
await nextTick()
|
|
expect(node.inputs.length).toBe(5)
|
|
|
|
connectInput(node, 0, graph)
|
|
expect(node.inputs.length).toBe(5)
|
|
node.disconnectInput(0)
|
|
await nextTick()
|
|
expect(node.inputs.length).toBe(5)
|
|
})
|
|
})
|