Files
ComfyUI_frontend/tests-ui/tests/widgets/dynamicCombo.test.ts
AustinMroz bc553f12be Add support for dynamic widgets (#6661)
Adds support for "dynamic combo" widgets where selecting a value on a
combo widget can cause other widgets or inputs to be created.


![dynamic-widgets_00001](https://github.com/user-attachments/assets/c797d008-f335-4d4e-9b2e-6fe4a7187ba7)

Includes a fairly large refactoring in litegraphService to remove
`#private` methods and cleanup some duplication in constructors for
subgraphNodes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6661-Add-support-for-dynamic-widgets-2a96d73d3650817aa570c7babbaca2f3)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2025-11-20 16:53:59 -07:00

91 lines
3.1 KiB
TypeScript

import { setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { describe, expect, test } from 'vitest'
import { 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 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 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')
expect(node.inputs[3].name).toBe('2.0.0')
})
})