mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
Tooltips are normally resolved through the node definition. Since DynamicCombo added widgets are nested in the spec definition, this lookup fails to find them. This PR makes it so that when a widget is dynamically added using `litegraphService:addNodeInput`, any 'tooltiptip defined in the provided inputSpec is applied on the widget. The tooltip system does not current support tooltips for dynamiclly added inputs. That can be considered for a followup PR. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9717-Support-tooltips-on-DynamicCombos-31f6d73d365081dc93f9eadd98572b3c) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
214 lines
6.9 KiB
TypeScript
214 lines
6.9 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, { tooltip: `${groupIndex}` }]
|
|
])
|
|
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')
|
|
})
|
|
test('Dynamically added widgets have tooltips', () => {
|
|
const node = testNode()
|
|
addDynamicCombo(node, [['INT'], ['STRING']])
|
|
expect.soft(node.widgets[1].tooltip).toBe('0')
|
|
node.widgets[0].value = '1'
|
|
expect.soft(node.widgets[1].tooltip).toBe('1')
|
|
})
|
|
})
|
|
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('0.test0')
|
|
expect(node.inputs[2].name).toBe('0.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('0.a')
|
|
expect(node.inputs[2].name).toBe('0.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)
|
|
})
|
|
test('Can deserialize a complex node', async () => {
|
|
const graph = new LGraph()
|
|
const node = testNode()
|
|
graph.add(node)
|
|
addAutogrow(node, { min: 1, input: inputsSpec, prefix: 'a' })
|
|
addAutogrow(node, { min: 1, input: inputsSpec, prefix: 'b' })
|
|
addNodeInput(node, { name: 'aa', isOptional: false, type: 'IMAGE' })
|
|
|
|
connectInput(node, 0, graph)
|
|
connectInput(node, 1, graph)
|
|
connectInput(node, 3, graph)
|
|
connectInput(node, 4, graph)
|
|
|
|
const serialized = graph.serialize()
|
|
graph.clear()
|
|
graph.configure(serialized)
|
|
const newNode = graph.nodes[0]!
|
|
|
|
expect(newNode.inputs.map((i) => i.name)).toStrictEqual([
|
|
'0.a0',
|
|
'0.a1',
|
|
'0.a2',
|
|
'1.b0',
|
|
'1.b1',
|
|
'1.b2',
|
|
'aa'
|
|
])
|
|
})
|
|
})
|