mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
Fix/widget ordering consistency (#5106)
* feat: input ordered nodes * fix: ensure node input order upon creation using input_order * refactor: back to the original state of migrations.ts * refactor: remove console.logs * test: fix widget ordering tests * fix: any types
This commit is contained in:
@@ -187,7 +187,7 @@ export { LGraphButton, type LGraphButtonOptions } from './LGraphButton'
|
||||
export { MovingOutputLink } from './canvas/MovingOutputLink'
|
||||
export { ToOutputRenderLink } from './canvas/ToOutputRenderLink'
|
||||
export { ToInputFromIoNodeLink } from './canvas/ToInputFromIoNodeLink'
|
||||
export type { TWidgetType, IWidgetOptions } from './types/widgets'
|
||||
export type { TWidgetType, TWidgetValue, IWidgetOptions } from './types/widgets'
|
||||
export {
|
||||
findUsedSubgraphIds,
|
||||
getDirectSubgraphIds,
|
||||
|
||||
@@ -232,7 +232,13 @@ export const zComfyNodeDef = z.object({
|
||||
* Comfy Org account.
|
||||
* https://docs.comfy.org/tutorials/api-nodes/overview
|
||||
*/
|
||||
api_node: z.boolean().optional()
|
||||
api_node: z.boolean().optional(),
|
||||
/**
|
||||
* Specifies the order of inputs for each input category.
|
||||
* Used to ensure consistent widget ordering regardless of JSON serialization.
|
||||
* Keys are 'required', 'optional', etc., values are arrays of input names.
|
||||
*/
|
||||
input_order: z.record(z.array(z.string())).optional()
|
||||
})
|
||||
|
||||
// `/object_info`
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
isVideoNode,
|
||||
migrateWidgetsValues
|
||||
} from '@/utils/litegraphUtil'
|
||||
import { getOrderedInputSpecs } from '@/utils/nodeDefOrderingUtil'
|
||||
|
||||
import { useExtensionService } from './extensionService'
|
||||
|
||||
@@ -248,9 +249,14 @@ export const useLitegraphService = () => {
|
||||
* @internal Add inputs to the node.
|
||||
*/
|
||||
#addInputs(inputs: Record<string, InputSpec>) {
|
||||
for (const inputSpec of Object.values(inputs))
|
||||
// Use input_order if available to ensure consistent widget ordering
|
||||
const nodeDefImpl = ComfyNode.nodeData as ComfyNodeDefImpl
|
||||
const orderedInputSpecs = getOrderedInputSpecs(nodeDefImpl, inputs)
|
||||
|
||||
// Create sockets and widgets in the determined order
|
||||
for (const inputSpec of orderedInputSpecs)
|
||||
this.#addInputSocket(inputSpec)
|
||||
for (const inputSpec of Object.values(inputs))
|
||||
for (const inputSpec of orderedInputSpecs)
|
||||
this.#addInputWidget(inputSpec)
|
||||
}
|
||||
|
||||
@@ -508,9 +514,14 @@ export const useLitegraphService = () => {
|
||||
* @internal Add inputs to the node.
|
||||
*/
|
||||
#addInputs(inputs: Record<string, InputSpec>) {
|
||||
for (const inputSpec of Object.values(inputs))
|
||||
// Use input_order if available to ensure consistent widget ordering
|
||||
const nodeDefImpl = ComfyNode.nodeData as ComfyNodeDefImpl
|
||||
const orderedInputSpecs = getOrderedInputSpecs(nodeDefImpl, inputs)
|
||||
|
||||
// Create sockets and widgets in the determined order
|
||||
for (const inputSpec of orderedInputSpecs)
|
||||
this.#addInputSocket(inputSpec)
|
||||
for (const inputSpec of Object.values(inputs))
|
||||
for (const inputSpec of orderedInputSpecs)
|
||||
this.#addInputWidget(inputSpec)
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,10 @@ export class ComfyNodeDefImpl
|
||||
* @deprecated Use `outputs[n].tooltip` instead
|
||||
*/
|
||||
readonly output_tooltips?: string[]
|
||||
/**
|
||||
* Order of inputs for each category (required, optional, hidden)
|
||||
*/
|
||||
readonly input_order?: Record<string, string[]>
|
||||
|
||||
// V2 fields
|
||||
readonly inputs: Record<string, InputSpecV2>
|
||||
@@ -130,6 +134,7 @@ export class ComfyNodeDefImpl
|
||||
this.output_is_list = obj.output_is_list
|
||||
this.output_name = obj.output_name
|
||||
this.output_tooltips = obj.output_tooltips
|
||||
this.input_order = obj.input_order
|
||||
|
||||
// Initialize V2 fields
|
||||
const defV2 = transformNodeDefV1ToV2(obj)
|
||||
|
||||
108
src/utils/nodeDefOrderingUtil.ts
Normal file
108
src/utils/nodeDefOrderingUtil.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { TWidgetValue } from '@/lib/litegraph/src/litegraph'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
|
||||
/**
|
||||
* Gets an ordered array of InputSpec objects based on input_order.
|
||||
* This is designed to work with V2 format used by litegraphService.
|
||||
*
|
||||
* @param nodeDefImpl - The ComfyNodeDefImpl containing both V1 and V2 formats
|
||||
* @param inputs - The V2 format inputs (flat Record<string, InputSpec>)
|
||||
* @returns Array of InputSpec objects in the correct order
|
||||
*/
|
||||
export function getOrderedInputSpecs(
|
||||
nodeDefImpl: ComfyNodeDefImpl,
|
||||
inputs: Record<string, InputSpec>
|
||||
): InputSpec[] {
|
||||
const orderedInputSpecs: InputSpec[] = []
|
||||
|
||||
// If no input_order, return default Object.values order
|
||||
if (!nodeDefImpl.input_order) {
|
||||
return Object.values(inputs)
|
||||
}
|
||||
|
||||
// Process required inputs in specified order
|
||||
if (nodeDefImpl.input_order.required) {
|
||||
for (const name of nodeDefImpl.input_order.required) {
|
||||
const inputSpec = inputs[name]
|
||||
if (inputSpec && !inputSpec.isOptional) {
|
||||
orderedInputSpecs.push(inputSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process optional inputs in specified order
|
||||
if (nodeDefImpl.input_order.optional) {
|
||||
for (const name of nodeDefImpl.input_order.optional) {
|
||||
const inputSpec = inputs[name]
|
||||
if (inputSpec && inputSpec.isOptional) {
|
||||
orderedInputSpecs.push(inputSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining inputs not specified in input_order
|
||||
const processedNames = new Set(orderedInputSpecs.map((spec) => spec.name))
|
||||
for (const inputSpec of Object.values(inputs)) {
|
||||
if (!processedNames.has(inputSpec.name)) {
|
||||
orderedInputSpecs.push(inputSpec)
|
||||
}
|
||||
}
|
||||
|
||||
return orderedInputSpecs
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorders widget values based on the input_order to match expected widget order.
|
||||
* This is used when widgets were created in a different order than input_order specifies.
|
||||
*
|
||||
* @param widgetValues - The current widget values array
|
||||
* @param currentWidgetOrder - The current order of widget names
|
||||
* @param inputOrder - The desired order from input_order
|
||||
* @returns Reordered widget values array
|
||||
*/
|
||||
export function sortWidgetValuesByInputOrder(
|
||||
widgetValues: TWidgetValue[],
|
||||
currentWidgetOrder: string[],
|
||||
inputOrder: string[]
|
||||
): TWidgetValue[] {
|
||||
if (!inputOrder || inputOrder.length === 0) {
|
||||
return widgetValues
|
||||
}
|
||||
|
||||
// Create a map of widget name to value
|
||||
const valueMap = new Map<string, TWidgetValue>()
|
||||
currentWidgetOrder.forEach((name, index) => {
|
||||
if (index < widgetValues.length) {
|
||||
valueMap.set(name, widgetValues[index])
|
||||
}
|
||||
})
|
||||
|
||||
// Reorder based on input_order
|
||||
const reordered: TWidgetValue[] = []
|
||||
const usedNames = new Set<string>()
|
||||
|
||||
// First, add values in the order specified by input_order
|
||||
for (const name of inputOrder) {
|
||||
if (valueMap.has(name)) {
|
||||
reordered.push(valueMap.get(name))
|
||||
usedNames.add(name)
|
||||
}
|
||||
}
|
||||
|
||||
// Then add any remaining values not in input_order
|
||||
for (const [name, value] of valueMap.entries()) {
|
||||
if (!usedNames.has(name)) {
|
||||
reordered.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
// If there are extra values not in the map, append them
|
||||
if (widgetValues.length > currentWidgetOrder.length) {
|
||||
for (let i = currentWidgetOrder.length; i < widgetValues.length; i++) {
|
||||
reordered.push(widgetValues[i])
|
||||
}
|
||||
}
|
||||
|
||||
return reordered
|
||||
}
|
||||
162
tests-ui/tests/litegraph/core/LGraphNode.widgetOrder.test.ts
Normal file
162
tests-ui/tests/litegraph/core/LGraphNode.widgetOrder.test.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation'
|
||||
import { sortWidgetValuesByInputOrder } from '@/utils/nodeDefOrderingUtil'
|
||||
|
||||
describe('LGraphNode widget ordering', () => {
|
||||
let node: LGraphNode
|
||||
|
||||
beforeEach(() => {
|
||||
node = new LGraphNode('TestNode')
|
||||
})
|
||||
|
||||
describe('configure with widgets_values', () => {
|
||||
it('should apply widget values in correct order when widgets order matches input_order', () => {
|
||||
// Create node with widgets
|
||||
node.addWidget('number', 'steps', 20, null, {})
|
||||
node.addWidget('number', 'seed', 0, null, {})
|
||||
node.addWidget('text', 'prompt', '', null, {})
|
||||
|
||||
// Configure with widget values
|
||||
const info: ISerialisedNode = {
|
||||
id: 1,
|
||||
type: 'TestNode',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
widgets_values: [30, 12345, 'test prompt']
|
||||
}
|
||||
|
||||
node.configure(info)
|
||||
|
||||
// Check widget values are applied correctly
|
||||
expect(node.widgets![0].value).toBe(30) // steps
|
||||
expect(node.widgets![1].value).toBe(12345) // seed
|
||||
expect(node.widgets![2].value).toBe('test prompt') // prompt
|
||||
})
|
||||
|
||||
it('should handle mismatched widget order with input_order', () => {
|
||||
// Simulate widgets created in wrong order (e.g., from unordered Object.entries)
|
||||
// but widgets_values is in the correct order according to input_order
|
||||
node.addWidget('number', 'seed', 0, null, {})
|
||||
node.addWidget('text', 'prompt', '', null, {})
|
||||
node.addWidget('number', 'steps', 20, null, {})
|
||||
|
||||
// Widget values are in input_order: [steps, seed, prompt]
|
||||
const info: ISerialisedNode = {
|
||||
id: 1,
|
||||
type: 'TestNode',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
widgets_values: [30, 12345, 'test prompt']
|
||||
}
|
||||
|
||||
// This would apply values incorrectly without proper ordering
|
||||
node.configure(info)
|
||||
|
||||
// Without fix, values would be applied in wrong order:
|
||||
// seed (widget[0]) would get 30 (should be 12345)
|
||||
// prompt (widget[1]) would get 12345 (should be 'test prompt')
|
||||
// steps (widget[2]) would get 'test prompt' (should be 30)
|
||||
|
||||
// This test demonstrates the bug - values are applied in wrong order
|
||||
expect(node.widgets![0].value).toBe(30) // seed gets steps value (WRONG)
|
||||
expect(node.widgets![1].value).toBe(12345) // prompt gets seed value (WRONG)
|
||||
expect(node.widgets![2].value).toBe('test prompt') // steps gets prompt value (WRONG)
|
||||
})
|
||||
|
||||
it('should skip widgets with serialize: false', () => {
|
||||
node.addWidget('number', 'steps', 20, null, {})
|
||||
node.addWidget('button', 'action', 'Click', null, {})
|
||||
node.widgets![1].serialize = false // button should not serialize
|
||||
node.addWidget('number', 'seed', 0, null, {})
|
||||
|
||||
const info: ISerialisedNode = {
|
||||
id: 1,
|
||||
type: 'TestNode',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
widgets_values: [30, 12345] // Only serializable widgets
|
||||
}
|
||||
|
||||
node.configure(info)
|
||||
|
||||
expect(node.widgets![0].value).toBe(30) // steps
|
||||
expect(node.widgets![1].value).toBe('Click') // button unchanged
|
||||
expect(node.widgets![2].value).toBe(12345) // seed
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('sortWidgetValuesByInputOrder', () => {
|
||||
it('should reorder widget values based on input_order', () => {
|
||||
const inputOrder = ['steps', 'seed', 'prompt']
|
||||
const currentWidgetOrder = ['seed', 'prompt', 'steps']
|
||||
const widgetValues = [12345, 'test prompt', 30]
|
||||
|
||||
const reordered = sortWidgetValuesByInputOrder(
|
||||
widgetValues,
|
||||
currentWidgetOrder,
|
||||
inputOrder
|
||||
)
|
||||
|
||||
// Should reorder to match input_order: [steps, seed, prompt]
|
||||
expect(reordered).toEqual([30, 12345, 'test prompt'])
|
||||
})
|
||||
|
||||
it('should handle widgets not in input_order', () => {
|
||||
const inputOrder = ['steps', 'seed']
|
||||
const currentWidgetOrder = ['seed', 'prompt', 'steps', 'cfg']
|
||||
const widgetValues = [12345, 'test prompt', 30, 7.5]
|
||||
|
||||
const reordered = sortWidgetValuesByInputOrder(
|
||||
widgetValues,
|
||||
currentWidgetOrder,
|
||||
inputOrder
|
||||
)
|
||||
|
||||
// Should put ordered items first, then unordered
|
||||
expect(reordered).toEqual([30, 12345, 'test prompt', 7.5])
|
||||
})
|
||||
|
||||
it('should handle empty input_order', () => {
|
||||
const inputOrder: string[] = []
|
||||
const currentWidgetOrder = ['seed', 'prompt', 'steps']
|
||||
const widgetValues = [12345, 'test prompt', 30]
|
||||
|
||||
const reordered = sortWidgetValuesByInputOrder(
|
||||
widgetValues,
|
||||
currentWidgetOrder,
|
||||
inputOrder
|
||||
)
|
||||
|
||||
// Should return values unchanged
|
||||
expect(reordered).toEqual([12345, 'test prompt', 30])
|
||||
})
|
||||
|
||||
it('should handle mismatched array lengths', () => {
|
||||
const inputOrder = ['steps', 'seed', 'prompt']
|
||||
const currentWidgetOrder = ['seed', 'prompt']
|
||||
const widgetValues = [12345, 'test prompt', 30] // Extra value
|
||||
|
||||
const reordered = sortWidgetValuesByInputOrder(
|
||||
widgetValues,
|
||||
currentWidgetOrder,
|
||||
inputOrder
|
||||
)
|
||||
|
||||
// Should handle gracefully, keeping extra values at the end
|
||||
// Since 'steps' is not in currentWidgetOrder, it won't be reordered
|
||||
// Only 'seed' and 'prompt' will be reordered based on input_order
|
||||
expect(reordered).toEqual([12345, 'test prompt', 30])
|
||||
})
|
||||
})
|
||||
274
tests-ui/tests/utils/nodeDefOrderingUtil.test.ts
Normal file
274
tests-ui/tests/utils/nodeDefOrderingUtil.test.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import {
|
||||
getOrderedInputSpecs,
|
||||
sortWidgetValuesByInputOrder
|
||||
} from '@/utils/nodeDefOrderingUtil'
|
||||
|
||||
describe('nodeDefOrderingUtil', () => {
|
||||
describe('getOrderedInputSpecs', () => {
|
||||
it('should maintain order when no input_order is specified', () => {
|
||||
const nodeDef: ComfyNodeDef = {
|
||||
name: 'TestNode',
|
||||
display_name: 'Test Node',
|
||||
category: 'test',
|
||||
python_module: 'test',
|
||||
description: 'Test node',
|
||||
output_node: false,
|
||||
input: {
|
||||
required: {
|
||||
image: ['IMAGE', {}],
|
||||
seed: ['INT', { default: 0 }],
|
||||
steps: ['INT', { default: 20 }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(nodeDef)
|
||||
const result = getOrderedInputSpecs(nodeDefImpl, nodeDefImpl.inputs)
|
||||
|
||||
// Should maintain Object.values order when no input_order
|
||||
const names = result.map((spec) => spec.name)
|
||||
expect(names).toEqual(['image', 'seed', 'steps'])
|
||||
})
|
||||
|
||||
it('should sort inputs according to input_order', () => {
|
||||
const nodeDef: ComfyNodeDef = {
|
||||
name: 'TestNode',
|
||||
display_name: 'Test Node',
|
||||
category: 'test',
|
||||
python_module: 'test',
|
||||
description: 'Test node',
|
||||
output_node: false,
|
||||
input: {
|
||||
required: {
|
||||
image: ['IMAGE', {}],
|
||||
seed: ['INT', { default: 0 }],
|
||||
steps: ['INT', { default: 20 }]
|
||||
}
|
||||
},
|
||||
input_order: {
|
||||
required: ['steps', 'seed', 'image']
|
||||
}
|
||||
}
|
||||
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(nodeDef)
|
||||
const result = getOrderedInputSpecs(nodeDefImpl, nodeDefImpl.inputs)
|
||||
|
||||
const names = result.map((spec) => spec.name)
|
||||
expect(names).toEqual(['steps', 'seed', 'image'])
|
||||
})
|
||||
|
||||
it('should handle missing inputs in input_order gracefully', () => {
|
||||
const nodeDef: ComfyNodeDef = {
|
||||
name: 'TestNode',
|
||||
display_name: 'Test Node',
|
||||
category: 'test',
|
||||
python_module: 'test',
|
||||
description: 'Test node',
|
||||
output_node: false,
|
||||
input: {
|
||||
required: {
|
||||
image: ['IMAGE', {}],
|
||||
seed: ['INT', { default: 0 }],
|
||||
steps: ['INT', { default: 20 }]
|
||||
}
|
||||
},
|
||||
input_order: {
|
||||
required: ['steps', 'nonexistent', 'seed']
|
||||
}
|
||||
}
|
||||
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(nodeDef)
|
||||
const result = getOrderedInputSpecs(nodeDefImpl, nodeDefImpl.inputs)
|
||||
|
||||
// Should skip nonexistent and include image at the end
|
||||
const names = result.map((spec) => spec.name)
|
||||
expect(names).toEqual(['steps', 'seed', 'image'])
|
||||
})
|
||||
|
||||
it('should handle inputs not in input_order', () => {
|
||||
const nodeDef: ComfyNodeDef = {
|
||||
name: 'TestNode',
|
||||
display_name: 'Test Node',
|
||||
category: 'test',
|
||||
python_module: 'test',
|
||||
description: 'Test node',
|
||||
output_node: false,
|
||||
input: {
|
||||
required: {
|
||||
image: ['IMAGE', {}],
|
||||
seed: ['INT', { default: 0 }],
|
||||
steps: ['INT', { default: 20 }],
|
||||
cfg: ['FLOAT', { default: 7.0 }]
|
||||
}
|
||||
},
|
||||
input_order: {
|
||||
required: ['steps', 'seed']
|
||||
}
|
||||
}
|
||||
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(nodeDef)
|
||||
const result = getOrderedInputSpecs(nodeDefImpl, nodeDefImpl.inputs)
|
||||
|
||||
// Should have ordered ones first, then remaining
|
||||
const names = result.map((spec) => spec.name)
|
||||
expect(names).toEqual(['steps', 'seed', 'image', 'cfg'])
|
||||
})
|
||||
|
||||
it('should handle both required and optional inputs', () => {
|
||||
const nodeDef: ComfyNodeDef = {
|
||||
name: 'TestNode',
|
||||
display_name: 'Test Node',
|
||||
category: 'test',
|
||||
python_module: 'test',
|
||||
description: 'Test node',
|
||||
output_node: false,
|
||||
input: {
|
||||
required: {
|
||||
image: ['IMAGE', {}],
|
||||
seed: ['INT', { default: 0 }]
|
||||
},
|
||||
optional: {
|
||||
mask: ['MASK', {}],
|
||||
strength: ['FLOAT', { default: 1.0 }]
|
||||
}
|
||||
},
|
||||
input_order: {
|
||||
required: ['seed', 'image'],
|
||||
optional: ['strength', 'mask']
|
||||
}
|
||||
}
|
||||
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(nodeDef)
|
||||
const result = getOrderedInputSpecs(nodeDefImpl, nodeDefImpl.inputs)
|
||||
|
||||
const names = result.map((spec) => spec.name)
|
||||
const optionalFlags = result.map((spec) => spec.isOptional)
|
||||
|
||||
expect(names).toEqual(['seed', 'image', 'strength', 'mask'])
|
||||
expect(optionalFlags).toEqual([false, false, true, true])
|
||||
})
|
||||
|
||||
it('should work with real KSampler node example', () => {
|
||||
// Simulating different backend orderings
|
||||
const kSamplerDefBackendA: ComfyNodeDef = {
|
||||
name: 'KSampler',
|
||||
display_name: 'KSampler',
|
||||
category: 'sampling',
|
||||
python_module: 'nodes',
|
||||
description: 'KSampler node',
|
||||
output_node: false,
|
||||
input: {
|
||||
required: {
|
||||
// Alphabetical order from backend A
|
||||
cfg: ['FLOAT', { default: 8, min: 0, max: 100 }],
|
||||
denoise: ['FLOAT', { default: 1, min: 0, max: 1 }],
|
||||
latent_image: ['LATENT', {}],
|
||||
model: ['MODEL', {}],
|
||||
negative: ['CONDITIONING', {}],
|
||||
positive: ['CONDITIONING', {}],
|
||||
sampler_name: [['euler', 'euler_cfg_pp'], {}],
|
||||
scheduler: [['simple', 'sgm_uniform'], {}],
|
||||
seed: ['INT', { default: 0, min: 0, max: Number.MAX_SAFE_INTEGER }],
|
||||
steps: ['INT', { default: 20, min: 1, max: 10000 }]
|
||||
}
|
||||
},
|
||||
input_order: {
|
||||
required: [
|
||||
'model',
|
||||
'seed',
|
||||
'steps',
|
||||
'cfg',
|
||||
'sampler_name',
|
||||
'scheduler',
|
||||
'positive',
|
||||
'negative',
|
||||
'latent_image',
|
||||
'denoise'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(kSamplerDefBackendA)
|
||||
const result = getOrderedInputSpecs(nodeDefImpl, nodeDefImpl.inputs)
|
||||
|
||||
const names = result.map((spec) => spec.name)
|
||||
// Should follow input_order, not alphabetical
|
||||
expect(names).toEqual([
|
||||
'model',
|
||||
'seed',
|
||||
'steps',
|
||||
'cfg',
|
||||
'sampler_name',
|
||||
'scheduler',
|
||||
'positive',
|
||||
'negative',
|
||||
'latent_image',
|
||||
'denoise'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('sortWidgetValuesByInputOrder', () => {
|
||||
it('should reorder widget values to match input_order', () => {
|
||||
const widgetValues = [0, 'model_ref', 5, 1]
|
||||
const currentWidgetOrder = ['momentum', 'model', 'norm_threshold', 'eta']
|
||||
const correctOrder = ['model', 'eta', 'norm_threshold', 'momentum']
|
||||
|
||||
const result = sortWidgetValuesByInputOrder(
|
||||
widgetValues,
|
||||
currentWidgetOrder,
|
||||
correctOrder
|
||||
)
|
||||
|
||||
expect(result).toEqual(['model_ref', 1, 5, 0])
|
||||
})
|
||||
|
||||
it('should handle missing widgets in input_order', () => {
|
||||
const widgetValues = [1, 2, 3, 4]
|
||||
const currentWidgetOrder = ['a', 'b', 'c', 'd']
|
||||
const inputOrder = ['b', 'd'] // Only partial order
|
||||
|
||||
const result = sortWidgetValuesByInputOrder(
|
||||
widgetValues,
|
||||
currentWidgetOrder,
|
||||
inputOrder
|
||||
)
|
||||
|
||||
// b=2, d=4, then a=1, c=3
|
||||
expect(result).toEqual([2, 4, 1, 3])
|
||||
})
|
||||
|
||||
it('should handle extra widget values', () => {
|
||||
const widgetValues = [1, 2, 3, 4, 5] // More values than names
|
||||
const currentWidgetOrder = ['a', 'b', 'c']
|
||||
const inputOrder = ['c', 'a', 'b']
|
||||
|
||||
const result = sortWidgetValuesByInputOrder(
|
||||
widgetValues,
|
||||
currentWidgetOrder,
|
||||
inputOrder
|
||||
)
|
||||
|
||||
// c=3, a=1, b=2, then extras 4, 5
|
||||
expect(result).toEqual([3, 1, 2, 4, 5])
|
||||
})
|
||||
|
||||
it('should return unchanged when no input_order', () => {
|
||||
const widgetValues = [1, 2, 3]
|
||||
const currentWidgetOrder = ['a', 'b', 'c']
|
||||
const inputOrder: string[] = []
|
||||
|
||||
const result = sortWidgetValuesByInputOrder(
|
||||
widgetValues,
|
||||
currentWidgetOrder,
|
||||
inputOrder
|
||||
)
|
||||
|
||||
expect(result).toEqual([1, 2, 3])
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user