Skip to content

Commit 36841d6

Browse files
test: add unit tests for nodeDefSchema
Addresses review feedback: #11464 (review)
1 parent bd49a23 commit 36841d6

1 file changed

Lines changed: 266 additions & 0 deletions

File tree

src/schemas/nodeDefSchema.test.ts

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import type {
4+
ComboInputSpec,
5+
ComboInputSpecV2,
6+
InputSpec
7+
} from './nodeDefSchema'
8+
import {
9+
getComboSpecComboOptions,
10+
getInputSpecType,
11+
isComboInputSpec,
12+
isComboInputSpecV1,
13+
isComboInputSpecV2,
14+
isFloatInputSpec,
15+
isIntInputSpec,
16+
isMediaUploadComboInput,
17+
validateComfyNodeDef
18+
} from './nodeDefSchema'
19+
20+
describe('nodeDefSchema', () => {
21+
describe('isComboInputSpecV1', () => {
22+
it('returns true for legacy combo spec with array', () => {
23+
const spec: InputSpec = [['option1', 'option2'], {}]
24+
expect(isComboInputSpecV1(spec)).toBe(true)
25+
})
26+
27+
it('returns false for v2 combo spec', () => {
28+
const spec: InputSpec = ['COMBO', { options: ['a', 'b'] }]
29+
expect(isComboInputSpecV1(spec)).toBe(false)
30+
})
31+
32+
it('returns false for INT spec', () => {
33+
const spec: InputSpec = ['INT', {}]
34+
expect(isComboInputSpecV1(spec)).toBe(false)
35+
})
36+
})
37+
38+
describe('isIntInputSpec', () => {
39+
it('returns true for INT spec', () => {
40+
const spec: InputSpec = ['INT', { min: 0, max: 100 }]
41+
expect(isIntInputSpec(spec)).toBe(true)
42+
})
43+
44+
it('returns false for FLOAT spec', () => {
45+
const spec: InputSpec = ['FLOAT', {}]
46+
expect(isIntInputSpec(spec)).toBe(false)
47+
})
48+
})
49+
50+
describe('isFloatInputSpec', () => {
51+
it('returns true for FLOAT spec', () => {
52+
const spec: InputSpec = ['FLOAT', { min: 0.0, max: 1.0 }]
53+
expect(isFloatInputSpec(spec)).toBe(true)
54+
})
55+
56+
it('returns false for INT spec', () => {
57+
const spec: InputSpec = ['INT', {}]
58+
expect(isFloatInputSpec(spec)).toBe(false)
59+
})
60+
})
61+
62+
describe('isComboInputSpecV2', () => {
63+
it('returns true for COMBO spec', () => {
64+
const spec: InputSpec = ['COMBO', { options: ['a', 'b'] }]
65+
expect(isComboInputSpecV2(spec)).toBe(true)
66+
})
67+
68+
it('returns false for legacy combo spec', () => {
69+
const spec: InputSpec = [['a', 'b'], {}]
70+
expect(isComboInputSpecV2(spec)).toBe(false)
71+
})
72+
})
73+
74+
describe('isComboInputSpec', () => {
75+
it('returns true for v1 combo spec', () => {
76+
const spec: InputSpec = [['option1', 'option2'], {}]
77+
expect(isComboInputSpec(spec)).toBe(true)
78+
})
79+
80+
it('returns true for v2 combo spec', () => {
81+
const spec: InputSpec = ['COMBO', { options: ['a', 'b'] }]
82+
expect(isComboInputSpec(spec)).toBe(true)
83+
})
84+
85+
it('returns false for INT spec', () => {
86+
const spec: InputSpec = ['INT', {}]
87+
expect(isComboInputSpec(spec)).toBe(false)
88+
})
89+
})
90+
91+
describe('isMediaUploadComboInput', () => {
92+
it('returns true for image_upload combo v1', () => {
93+
const spec: InputSpec = [['img1.png', 'img2.png'], { image_upload: true }]
94+
expect(isMediaUploadComboInput(spec)).toBe(true)
95+
})
96+
97+
it('returns true for video_upload combo v2', () => {
98+
const spec: InputSpec = ['COMBO', { video_upload: true }]
99+
expect(isMediaUploadComboInput(spec)).toBe(true)
100+
})
101+
102+
it('returns true for animated_image_upload combo', () => {
103+
const spec: InputSpec = [['gif1.gif'], { animated_image_upload: true }]
104+
expect(isMediaUploadComboInput(spec)).toBe(true)
105+
})
106+
107+
it('returns false when options is undefined', () => {
108+
const spec: InputSpec = ['COMBO', undefined]
109+
expect(isMediaUploadComboInput(spec)).toBe(false)
110+
})
111+
112+
it('returns false for non-upload combo', () => {
113+
const spec: InputSpec = ['COMBO', { options: ['a', 'b'] }]
114+
expect(isMediaUploadComboInput(spec)).toBe(false)
115+
})
116+
117+
it('returns false for INT with upload flag', () => {
118+
const spec: InputSpec = ['INT', { image_upload: true } as never]
119+
expect(isMediaUploadComboInput(spec)).toBe(false)
120+
})
121+
})
122+
123+
describe('getInputSpecType', () => {
124+
it('returns COMBO for v1 combo spec', () => {
125+
const spec: InputSpec = [['a', 'b'], {}]
126+
expect(getInputSpecType(spec)).toBe('COMBO')
127+
})
128+
129+
it('returns COMBO for v2 combo spec', () => {
130+
const spec: InputSpec = ['COMBO', {}]
131+
expect(getInputSpecType(spec)).toBe('COMBO')
132+
})
133+
134+
it('returns INT for INT spec', () => {
135+
const spec: InputSpec = ['INT', {}]
136+
expect(getInputSpecType(spec)).toBe('INT')
137+
})
138+
139+
it('returns FLOAT for FLOAT spec', () => {
140+
const spec: InputSpec = ['FLOAT', {}]
141+
expect(getInputSpecType(spec)).toBe('FLOAT')
142+
})
143+
144+
it('returns custom type for custom spec', () => {
145+
const spec: InputSpec = ['CUSTOM_TYPE', {}]
146+
expect(getInputSpecType(spec)).toBe('CUSTOM_TYPE')
147+
})
148+
})
149+
150+
describe('getComboSpecComboOptions', () => {
151+
it('returns options from v1 combo spec', () => {
152+
const spec: ComboInputSpec = [['opt1', 'opt2', 123], {}]
153+
expect(getComboSpecComboOptions(spec)).toEqual(['opt1', 'opt2', 123])
154+
})
155+
156+
it('returns options from v2 combo spec', () => {
157+
const spec: ComboInputSpecV2 = ['COMBO', { options: ['a', 'b', 'c'] }]
158+
expect(getComboSpecComboOptions(spec)).toEqual(['a', 'b', 'c'])
159+
})
160+
161+
it('returns empty array when v2 has no options', () => {
162+
const spec: ComboInputSpecV2 = ['COMBO', {}]
163+
expect(getComboSpecComboOptions(spec)).toEqual([])
164+
})
165+
166+
it('returns empty array when v2 options is undefined', () => {
167+
const spec: ComboInputSpecV2 = ['COMBO', undefined]
168+
expect(getComboSpecComboOptions(spec)).toEqual([])
169+
})
170+
})
171+
172+
describe('validateComfyNodeDef', () => {
173+
const validNodeDef = {
174+
name: 'TestNode',
175+
display_name: 'Test Node',
176+
description: 'A test node',
177+
category: 'test',
178+
output_node: false,
179+
python_module: 'test_module'
180+
}
181+
182+
it('returns validated node def for valid input', () => {
183+
const result = validateComfyNodeDef(validNodeDef)
184+
expect(result).toEqual(validNodeDef)
185+
})
186+
187+
it('returns node def with optional fields', () => {
188+
const nodeWithOptional = {
189+
...validNodeDef,
190+
deprecated: true,
191+
experimental: true,
192+
api_node: true,
193+
help: 'Some help text'
194+
}
195+
const result = validateComfyNodeDef(nodeWithOptional)
196+
expect(result).toEqual(nodeWithOptional)
197+
})
198+
199+
it('returns null and calls onError for invalid input', () => {
200+
const onError = vi.fn()
201+
const invalidDef = { name: 'Test' }
202+
const result = validateComfyNodeDef(invalidDef, onError)
203+
expect(result).toBeNull()
204+
expect(onError).toHaveBeenCalled()
205+
})
206+
207+
it('uses console.warn as default error handler', () => {
208+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
209+
const invalidDef = { name: 'Test' }
210+
validateComfyNodeDef(invalidDef)
211+
expect(warnSpy).toHaveBeenCalled()
212+
warnSpy.mockRestore()
213+
})
214+
215+
it('validates input specs correctly', () => {
216+
const nodeWithInputs = {
217+
...validNodeDef,
218+
input: {
219+
required: {
220+
seed: ['INT', { min: 0, max: 1000 }],
221+
prompt: ['STRING', { multiline: true }]
222+
},
223+
optional: {
224+
model: ['COMBO', { options: ['model1', 'model2'] }]
225+
}
226+
}
227+
}
228+
const result = validateComfyNodeDef(nodeWithInputs)
229+
expect(result).not.toBeNull()
230+
expect(result?.input?.required?.seed).toEqual([
231+
'INT',
232+
{ min: 0, max: 1000 }
233+
])
234+
})
235+
236+
it('validates output specs correctly', () => {
237+
const nodeWithOutputs = {
238+
...validNodeDef,
239+
output: ['IMAGE', 'MASK'],
240+
output_name: ['image', 'mask'],
241+
output_is_list: [false, false]
242+
}
243+
const result = validateComfyNodeDef(nodeWithOutputs)
244+
expect(result).not.toBeNull()
245+
expect(result?.output).toEqual(['IMAGE', 'MASK'])
246+
})
247+
248+
it('validates price_badge correctly', () => {
249+
const nodeWithPriceBadge = {
250+
...validNodeDef,
251+
api_node: true,
252+
price_badge: {
253+
engine: 'jsonata',
254+
depends_on: {
255+
widgets: [{ name: 'width', type: 'INT' }],
256+
inputs: ['image']
257+
},
258+
expr: '$round(w.width * 0.001, 4)'
259+
}
260+
}
261+
const result = validateComfyNodeDef(nodeWithPriceBadge)
262+
expect(result).not.toBeNull()
263+
expect(result?.price_badge?.expr).toBe('$round(w.width * 0.001, 4)')
264+
})
265+
})
266+
})

0 commit comments

Comments
 (0)