mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
[Refactor] Add util to merge input spec (#2834)
This commit is contained in:
@@ -3,8 +3,8 @@ import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type InputSpec,
|
type InputSpec,
|
||||||
isComboInputSpec,
|
getComboSpecComboOptions,
|
||||||
isComboInputSpecV2
|
isComboInputSpec
|
||||||
} from '@/schemas/nodeDefSchema'
|
} from '@/schemas/nodeDefSchema'
|
||||||
import { addValueControlWidgets } from '@/scripts/widgets'
|
import { addValueControlWidgets } from '@/scripts/widgets'
|
||||||
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||||
@@ -24,9 +24,7 @@ export const useComboWidget = () => {
|
|||||||
|
|
||||||
const widgetStore = useWidgetStore()
|
const widgetStore = useWidgetStore()
|
||||||
const inputOptions = inputData[1] ?? {}
|
const inputOptions = inputData[1] ?? {}
|
||||||
const comboOptions =
|
const comboOptions = getComboSpecComboOptions(inputData)
|
||||||
(isComboInputSpecV2(inputData) ? inputOptions.options : inputData[0]) ??
|
|
||||||
[]
|
|
||||||
|
|
||||||
const defaultValue = widgetStore.getDefaultValue(inputData)
|
const defaultValue = widgetStore.getDefaultValue(inputData)
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import type { CanvasMouseEvent } from '@comfyorg/litegraph/dist/types/events'
|
|||||||
|
|
||||||
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { clone } from '@/scripts/utils'
|
|
||||||
import { ComfyWidgets, addValueControlWidgets } from '@/scripts/widgets'
|
import { ComfyWidgets, addValueControlWidgets } from '@/scripts/widgets'
|
||||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
|
import { mergeInputSpec } from '@/utils/nodeDefUtil'
|
||||||
import { applyTextReplacements } from '@/utils/searchAndReplace'
|
import { applyTextReplacements } from '@/utils/searchAndReplace'
|
||||||
import { isPrimitiveNode } from '@/utils/typeGuardUtil'
|
import { isPrimitiveNode } from '@/utils/typeGuardUtil'
|
||||||
|
|
||||||
@@ -603,113 +603,17 @@ export function mergeIfValid(
|
|||||||
config2: InputSpec,
|
config2: InputSpec,
|
||||||
forceUpdate?: boolean,
|
forceUpdate?: boolean,
|
||||||
recreateWidget?: () => void,
|
recreateWidget?: () => void,
|
||||||
config1?: unknown
|
config1?: InputSpec
|
||||||
): { customConfig: Record<string, unknown> } {
|
): { customConfig: InputSpec[1] } {
|
||||||
if (!config1) {
|
if (!config1) {
|
||||||
config1 = getWidgetConfig(output)
|
config1 = getWidgetConfig(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config1[0] instanceof Array) {
|
const customSpec = mergeInputSpec(config1, config2)
|
||||||
if (!isValidCombo(config1[0], config2[0])) return
|
|
||||||
} else if (config1[0] !== config2[0]) {
|
|
||||||
// Types dont match
|
|
||||||
console.log(`connection rejected: types dont match`, config1[0], config2[0])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = new Set([
|
if (customSpec || forceUpdate) {
|
||||||
...Object.keys(config1[1] ?? {}),
|
if (customSpec) {
|
||||||
...Object.keys(config2[1] ?? {})
|
output.widget[CONFIG] = customSpec
|
||||||
])
|
|
||||||
|
|
||||||
let customConfig: Record<string, unknown> | undefined
|
|
||||||
const getCustomConfig = () => {
|
|
||||||
if (!customConfig) {
|
|
||||||
customConfig = clone(config1[1] ?? {})
|
|
||||||
}
|
|
||||||
return customConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNumber = config1[0] === 'INT' || config1[0] === 'FLOAT'
|
|
||||||
for (const k of keys.values()) {
|
|
||||||
if (
|
|
||||||
k !== 'default' &&
|
|
||||||
k !== 'forceInput' &&
|
|
||||||
k !== 'defaultInput' &&
|
|
||||||
k !== 'control_after_generate' &&
|
|
||||||
k !== 'multiline' &&
|
|
||||||
k !== 'tooltip' &&
|
|
||||||
k !== 'dynamicPrompts'
|
|
||||||
) {
|
|
||||||
let v1 = config1[1][k]
|
|
||||||
let v2 = config2[1]?.[k]
|
|
||||||
|
|
||||||
if (v1 === v2 || (!v1 && !v2)) continue
|
|
||||||
|
|
||||||
if (isNumber) {
|
|
||||||
if (k === 'min') {
|
|
||||||
const theirMax = config2[1]?.['max']
|
|
||||||
if (theirMax != null && v1 > theirMax) {
|
|
||||||
console.log('connection rejected: min > max', v1, theirMax)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
getCustomConfig()[k] =
|
|
||||||
// @ts-expect-error InputSpec is not typed correctly
|
|
||||||
v1 == null ? v2 : v2 == null ? v1 : Math.max(v1, v2)
|
|
||||||
continue
|
|
||||||
} else if (k === 'max') {
|
|
||||||
const theirMin = config2[1]?.['min']
|
|
||||||
if (theirMin != null && v1 < theirMin) {
|
|
||||||
console.log('connection rejected: max < min', v1, theirMin)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
getCustomConfig()[k] =
|
|
||||||
// @ts-expect-error InputSpec is not typed correctly
|
|
||||||
v1 == null ? v2 : v2 == null ? v1 : Math.min(v1, v2)
|
|
||||||
continue
|
|
||||||
} else if (k === 'step') {
|
|
||||||
let step
|
|
||||||
if (v1 == null) {
|
|
||||||
// No current step
|
|
||||||
step = v2
|
|
||||||
} else if (v2 == null) {
|
|
||||||
// No new step
|
|
||||||
step = v1
|
|
||||||
} else {
|
|
||||||
if (v1 < v2) {
|
|
||||||
// Ensure v1 is larger for the mod
|
|
||||||
const a = v2
|
|
||||||
v2 = v1
|
|
||||||
v1 = a
|
|
||||||
}
|
|
||||||
// @ts-expect-error InputSpec is not typed correctly
|
|
||||||
if (v1 % v2) {
|
|
||||||
console.log(
|
|
||||||
'connection rejected: steps not divisible',
|
|
||||||
'current:',
|
|
||||||
v1,
|
|
||||||
'new:',
|
|
||||||
v2
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
step = v1
|
|
||||||
}
|
|
||||||
|
|
||||||
getCustomConfig()[k] = step
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`connection rejected: config ${k} values dont match`, v1, v2)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customConfig || forceUpdate) {
|
|
||||||
if (customConfig) {
|
|
||||||
output.widget[CONFIG] = [config1[0], customConfig]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const widget = recreateWidget?.call(this)
|
const widget = recreateWidget?.call(this)
|
||||||
@@ -723,7 +627,7 @@ export function mergeIfValid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { customConfig }
|
return { customConfig: customSpec[1] }
|
||||||
}
|
}
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const zRemoteWidgetConfig = z.object({
|
|||||||
const zBaseInputOptions = z
|
const zBaseInputOptions = z
|
||||||
.object({
|
.object({
|
||||||
default: z.any().optional(),
|
default: z.any().optional(),
|
||||||
|
/** @deprecated Group node uses this field. Remove when group node feature is removed. */
|
||||||
defaultInput: z.boolean().optional(),
|
defaultInput: z.boolean().optional(),
|
||||||
forceInput: z.boolean().optional(),
|
forceInput: z.boolean().optional(),
|
||||||
tooltip: z.string().optional(),
|
tooltip: z.string().optional(),
|
||||||
@@ -143,6 +144,30 @@ export function isComboInputSpec(
|
|||||||
return isComboInputSpecV1(inputSpec) || isComboInputSpecV2(inputSpec)
|
return isComboInputSpecV1(inputSpec) || isComboInputSpecV2(inputSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type of an input spec.
|
||||||
|
*
|
||||||
|
* @param inputSpec - The input spec to get the type of.
|
||||||
|
* @returns The type of the input spec.
|
||||||
|
*/
|
||||||
|
export function getInputSpecType(inputSpec: InputSpec): string {
|
||||||
|
return isComboInputSpec(inputSpec) ? 'COMBO' : inputSpec[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the combo options from a combo input spec.
|
||||||
|
*
|
||||||
|
* @param inputSpec - The input spec to get the combo options from.
|
||||||
|
* @returns The combo options.
|
||||||
|
*/
|
||||||
|
export function getComboSpecComboOptions(
|
||||||
|
inputSpec: ComboInputSpec | ComboInputSpecV2
|
||||||
|
): (number | string)[] {
|
||||||
|
return (
|
||||||
|
(isComboInputSpecV2(inputSpec) ? inputSpec[1]?.options : inputSpec[0]) ?? []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const excludedLiterals = new Set(['INT', 'FLOAT', 'BOOLEAN', 'STRING', 'COMBO'])
|
const excludedLiterals = new Set(['INT', 'FLOAT', 'BOOLEAN', 'STRING', 'COMBO'])
|
||||||
const zCustomInputSpec = z.tuple([
|
const zCustomInputSpec = z.tuple([
|
||||||
z.string().refine((value) => !excludedLiterals.has(value)),
|
z.string().refine((value) => !excludedLiterals.has(value)),
|
||||||
@@ -201,6 +226,8 @@ export type FloatInputOptions = z.infer<typeof zFloatInputOptions>
|
|||||||
export type BooleanInputOptions = z.infer<typeof zBooleanInputOptions>
|
export type BooleanInputOptions = z.infer<typeof zBooleanInputOptions>
|
||||||
export type StringInputOptions = z.infer<typeof zStringInputOptions>
|
export type StringInputOptions = z.infer<typeof zStringInputOptions>
|
||||||
export type ComboInputOptions = z.infer<typeof zComboInputOptions>
|
export type ComboInputOptions = z.infer<typeof zComboInputOptions>
|
||||||
|
export type BaseInputOptions = z.infer<typeof zBaseInputOptions>
|
||||||
|
export type NumericInputOptions = z.infer<typeof zNumericInputOptions>
|
||||||
|
|
||||||
export type IntInputSpec = z.infer<typeof zIntInputSpec>
|
export type IntInputSpec = z.infer<typeof zIntInputSpec>
|
||||||
export type FloatInputSpec = z.infer<typeof zFloatInputSpec>
|
export type FloatInputSpec = z.infer<typeof zFloatInputSpec>
|
||||||
|
|||||||
21
src/utils/mathUtil.ts
Normal file
21
src/utils/mathUtil.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Finds the greatest common divisor (GCD) for two numbers.
|
||||||
|
*
|
||||||
|
* @param a - The first number.
|
||||||
|
* @param b - The second number.
|
||||||
|
* @returns The GCD of the two numbers.
|
||||||
|
*/
|
||||||
|
export const gcd = (a: number, b: number): number => {
|
||||||
|
return b === 0 ? a : gcd(b, a % b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the least common multiple (LCM) for two numbers.
|
||||||
|
*
|
||||||
|
* @param a - The first number.
|
||||||
|
* @param b - The second number.
|
||||||
|
* @returns The LCM of the two numbers.
|
||||||
|
*/
|
||||||
|
export const lcm = (a: number, b: number): number => {
|
||||||
|
return Math.abs(a * b) / gcd(a, b)
|
||||||
|
}
|
||||||
142
src/utils/nodeDefUtil.ts
Normal file
142
src/utils/nodeDefUtil.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ComboInputSpec,
|
||||||
|
ComboInputSpecV2,
|
||||||
|
FloatInputSpec,
|
||||||
|
InputSpec,
|
||||||
|
IntInputSpec,
|
||||||
|
NumericInputOptions
|
||||||
|
} from '@/schemas/nodeDefSchema'
|
||||||
|
import {
|
||||||
|
getComboSpecComboOptions,
|
||||||
|
getInputSpecType,
|
||||||
|
isComboInputSpec,
|
||||||
|
isFloatInputSpec,
|
||||||
|
isIntInputSpec
|
||||||
|
} from '@/schemas/nodeDefSchema'
|
||||||
|
|
||||||
|
import { lcm } from './mathUtil'
|
||||||
|
|
||||||
|
const IGNORE_KEYS = new Set<string>([
|
||||||
|
'default',
|
||||||
|
'forceInput',
|
||||||
|
'defaultInput',
|
||||||
|
'control_after_generate',
|
||||||
|
'multiline',
|
||||||
|
'tooltip',
|
||||||
|
'dynamicPrompts'
|
||||||
|
])
|
||||||
|
|
||||||
|
const getRange = (options: NumericInputOptions) => {
|
||||||
|
const min = options.min ?? -Infinity
|
||||||
|
const max = options.max ?? Infinity
|
||||||
|
return { min, max }
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeNumericInputSpec = <T extends IntInputSpec | FloatInputSpec>(
|
||||||
|
spec1: T,
|
||||||
|
spec2: T
|
||||||
|
): T | null => {
|
||||||
|
const type = spec1[0]
|
||||||
|
const options1 = spec1[1] ?? {}
|
||||||
|
const options2 = spec2[1] ?? {}
|
||||||
|
|
||||||
|
const range1 = getRange(options1)
|
||||||
|
const range2 = getRange(options2)
|
||||||
|
|
||||||
|
// If the ranges do not overlap, return null
|
||||||
|
if (range1.min > range2.max || range1.max < range2.min) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const step1 = options1.step ?? 1
|
||||||
|
const step2 = options2.step ?? 1
|
||||||
|
|
||||||
|
const mergedOptions = {
|
||||||
|
// Take intersection of ranges
|
||||||
|
min: Math.max(range1.min, range2.min),
|
||||||
|
max: Math.min(range1.max, range2.max),
|
||||||
|
step: lcm(step1, step2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeCommonInputSpec(
|
||||||
|
[type, { ...options1, ...mergedOptions }] as unknown as T,
|
||||||
|
[type, { ...options2, ...mergedOptions }] as unknown as T
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeComboInputSpec = <T extends ComboInputSpec | ComboInputSpecV2>(
|
||||||
|
spec1: T,
|
||||||
|
spec2: T
|
||||||
|
): T | null => {
|
||||||
|
const options1 = spec1[1] ?? {}
|
||||||
|
const options2 = spec2[1] ?? {}
|
||||||
|
|
||||||
|
const comboOptions1 = getComboSpecComboOptions(spec1)
|
||||||
|
const comboOptions2 = getComboSpecComboOptions(spec2)
|
||||||
|
|
||||||
|
const intersection = _.intersection(comboOptions1, comboOptions2)
|
||||||
|
|
||||||
|
// If the intersection is empty, return null
|
||||||
|
if (intersection.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeCommonInputSpec(
|
||||||
|
['COMBO', { ...options1, options: intersection }] as unknown as T,
|
||||||
|
['COMBO', { ...options2, options: intersection }] as unknown as T
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeCommonInputSpec = <T extends InputSpec>(
|
||||||
|
spec1: T,
|
||||||
|
spec2: T
|
||||||
|
): T | null => {
|
||||||
|
const type = getInputSpecType(spec1)
|
||||||
|
const options1 = spec1[1] ?? {}
|
||||||
|
const options2 = spec2[1] ?? {}
|
||||||
|
|
||||||
|
const compareKeys = _.union(_.keys(options1), _.keys(options2)).filter(
|
||||||
|
(key) => !IGNORE_KEYS.has(key)
|
||||||
|
)
|
||||||
|
|
||||||
|
const mergeIsValid = compareKeys.every((key) => {
|
||||||
|
const value1 = options1[key]
|
||||||
|
const value2 = options2[key]
|
||||||
|
return value1 === value2 || (_.isNil(value1) && _.isNil(value2))
|
||||||
|
})
|
||||||
|
|
||||||
|
return mergeIsValid
|
||||||
|
? ([type, { ...options1, ...options2 }] as unknown as T)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges two input specs.
|
||||||
|
*
|
||||||
|
* @param spec1 - The first input spec.
|
||||||
|
* @param spec2 - The second input spec.
|
||||||
|
* @returns The merged input spec, or null if the specs are not mergeable.
|
||||||
|
*/
|
||||||
|
export const mergeInputSpec = (
|
||||||
|
spec1: InputSpec,
|
||||||
|
spec2: InputSpec
|
||||||
|
): InputSpec | null => {
|
||||||
|
const type1 = getInputSpecType(spec1)
|
||||||
|
const type2 = getInputSpecType(spec2)
|
||||||
|
|
||||||
|
if (type1 !== type2) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIntInputSpec(spec1) || isFloatInputSpec(spec1)) {
|
||||||
|
return mergeNumericInputSpec(spec1, spec2 as typeof spec1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isComboInputSpec(spec1)) {
|
||||||
|
return mergeComboInputSpec(spec1, spec2 as typeof spec1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeCommonInputSpec(spec1, spec2)
|
||||||
|
}
|
||||||
206
tests-ui/tests/utils/nodeDefUtil.test.ts
Normal file
206
tests-ui/tests/utils/nodeDefUtil.test.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
// @ts-strict-ignore
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ComboInputSpec,
|
||||||
|
ComboInputSpecV2,
|
||||||
|
FloatInputSpec,
|
||||||
|
InputSpec,
|
||||||
|
IntInputSpec
|
||||||
|
} from '@/schemas/nodeDefSchema'
|
||||||
|
import { mergeInputSpec } from '@/utils/nodeDefUtil'
|
||||||
|
|
||||||
|
describe('nodeDefUtil', () => {
|
||||||
|
describe('mergeInputSpec', () => {
|
||||||
|
// Test numeric input specs (INT and FLOAT)
|
||||||
|
describe('numeric input specs', () => {
|
||||||
|
it('should merge INT specs with overlapping ranges', () => {
|
||||||
|
const spec1: IntInputSpec = ['INT', { min: 0, max: 10 }]
|
||||||
|
const spec2: IntInputSpec = ['INT', { min: 5, max: 15 }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result?.[0]).toBe('INT')
|
||||||
|
expect(result?.[1].min).toBe(5)
|
||||||
|
expect(result?.[1].max).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null for INT specs with non-overlapping ranges', () => {
|
||||||
|
const spec1: IntInputSpec = ['INT', { min: 0, max: 5 }]
|
||||||
|
const spec2: IntInputSpec = ['INT', { min: 10, max: 15 }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should merge FLOAT specs with overlapping ranges', () => {
|
||||||
|
const spec1: FloatInputSpec = ['FLOAT', { min: 0.5, max: 10.5 }]
|
||||||
|
const spec2: FloatInputSpec = ['FLOAT', { min: 5.5, max: 15.5 }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result?.[0]).toBe('FLOAT')
|
||||||
|
expect(result?.[1].min).toBe(5.5)
|
||||||
|
expect(result?.[1].max).toBe(10.5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle specs with undefined min/max values', () => {
|
||||||
|
const spec1: FloatInputSpec = ['FLOAT', { min: 0.5 }]
|
||||||
|
const spec2: FloatInputSpec = ['FLOAT', { max: 15.5 }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result?.[0]).toBe('FLOAT')
|
||||||
|
expect(result?.[1].min).toBe(0.5)
|
||||||
|
expect(result?.[1].max).toBe(15.5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should merge step values using least common multiple', () => {
|
||||||
|
const spec1: IntInputSpec = ['INT', { min: 0, max: 10, step: 2 }]
|
||||||
|
const spec2: IntInputSpec = ['INT', { min: 0, max: 10, step: 3 }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result?.[0]).toBe('INT')
|
||||||
|
expect(result?.[1].step).toBe(6) // LCM of 2 and 3 is 6
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use default step of 1 when step is not specified', () => {
|
||||||
|
const spec1: IntInputSpec = ['INT', { min: 0, max: 10 }]
|
||||||
|
const spec2: IntInputSpec = ['INT', { min: 0, max: 10, step: 4 }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result?.[0]).toBe('INT')
|
||||||
|
expect(result?.[1].step).toBe(4) // LCM of 1 and 4 is 4
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle step values for FLOAT specs', () => {
|
||||||
|
const spec1: FloatInputSpec = ['FLOAT', { min: 0, max: 10, step: 0.5 }]
|
||||||
|
const spec2: FloatInputSpec = ['FLOAT', { min: 0, max: 10, step: 0.25 }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result?.[0]).toBe('FLOAT')
|
||||||
|
expect(result?.[1].step).toBe(0.5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test combo input specs
|
||||||
|
describe('combo input specs', () => {
|
||||||
|
it('should merge COMBO specs with overlapping options', () => {
|
||||||
|
const spec1: ComboInputSpecV2 = ['COMBO', { options: ['A', 'B', 'C'] }]
|
||||||
|
const spec2: ComboInputSpecV2 = ['COMBO', { options: ['B', 'C', 'D'] }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result?.[0]).toBe('COMBO')
|
||||||
|
expect(result?.[1].options).toEqual(['B', 'C'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null for COMBO specs with no overlapping options', () => {
|
||||||
|
const spec1: ComboInputSpecV2 = ['COMBO', { options: ['A', 'B'] }]
|
||||||
|
const spec2: ComboInputSpecV2 = ['COMBO', { options: ['C', 'D'] }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle COMBO specs with additional properties', () => {
|
||||||
|
const spec1: ComboInputSpecV2 = [
|
||||||
|
'COMBO',
|
||||||
|
{
|
||||||
|
options: ['A', 'B', 'C'],
|
||||||
|
default: 'A',
|
||||||
|
tooltip: 'Select an option'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const spec2: ComboInputSpecV2 = [
|
||||||
|
'COMBO',
|
||||||
|
{
|
||||||
|
options: ['B', 'C', 'D'],
|
||||||
|
default: 'B',
|
||||||
|
multiline: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result?.[0]).toBe('COMBO')
|
||||||
|
expect(result?.[1].options).toEqual(['B', 'C'])
|
||||||
|
expect(result?.[1].default).toBe('B')
|
||||||
|
expect(result?.[1].tooltip).toBe('Select an option')
|
||||||
|
expect(result?.[1].multiline).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle v1 and v2 combo specs', () => {
|
||||||
|
const spec1: ComboInputSpec = [['A', 'B', 'C', 'D'], {}]
|
||||||
|
const spec2: ComboInputSpecV2 = ['COMBO', { options: ['C', 'D'] }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result?.[0]).toBe('COMBO')
|
||||||
|
expect(result?.[1].options).toEqual(['C', 'D'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test common input spec behavior
|
||||||
|
describe('common input spec behavior', () => {
|
||||||
|
it('should return null for specs with different types', () => {
|
||||||
|
const spec1: IntInputSpec = ['INT', { min: 0, max: 10 }]
|
||||||
|
const spec2: ComboInputSpecV2 = ['COMBO', { options: ['A', 'B'] }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2 as unknown as IntInputSpec)
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ignore specified keys when comparing specs', () => {
|
||||||
|
const spec1: InputSpec = [
|
||||||
|
'STRING',
|
||||||
|
{
|
||||||
|
default: 'value1',
|
||||||
|
tooltip: 'Tooltip 1',
|
||||||
|
step: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const spec2: InputSpec = [
|
||||||
|
'STRING',
|
||||||
|
{
|
||||||
|
default: 'value2',
|
||||||
|
tooltip: 'Tooltip 2',
|
||||||
|
step: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result?.[0]).toBe('STRING')
|
||||||
|
expect(result?.[1].default).toBe('value2')
|
||||||
|
expect(result?.[1].tooltip).toBe('Tooltip 2')
|
||||||
|
expect(result?.[1].step).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null if non-ignored properties differ', () => {
|
||||||
|
const spec1: InputSpec = ['STRING', { step: 1 }]
|
||||||
|
const spec2: InputSpec = ['STRING', { step: 2 }]
|
||||||
|
|
||||||
|
const result = mergeInputSpec(spec1, spec2)
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user