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((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 = [ '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 = 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> } 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) }) })