mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 06:10:32 +00:00
refactor(schema): flatten V2 input spec defaults to top-level
The V1 to V2 input spec migration spreads input options at the top level of the spec, but the V2 schema declared default (COLOR), default+rows+cols (TEXTAREA), and content (MARKDOWN) nested under a separate `options` object. Two shapes, one wire format — silent fallback bugs like FE-800 were the inevitable consequence. Canonicalize on the top-level shape that the migration already produces: - nodeDefSchemaV2: hoist default to top level for COLOR/MARKDOWN; hoist rows/cols/default to top level for TEXTAREA. Drop the now-empty nested options objects on those three. Leave CHART/GALLERIA/IMAGE/IMAGECOMPARE alone — they are V2-native (no V1 migration path) and their nested options bag carries through to the runtime widget's options. - useColorWidget: single top-level read, no fallback chain. - useTextareaWidget: read rows/cols/default at the top level. - useColorWidget.test: drop the V1-vs-V2-shape parity cases; the schema now has one shape, so the tests pin the single contract.
This commit is contained in:
@@ -2,11 +2,10 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { ColorInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useColorWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useColorWidget'
|
||||
import type { ColorInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
const TOP_LEVEL_DEFAULT = '#00ff00'
|
||||
const NESTED_DEFAULT = '#abcdef'
|
||||
const DECLARED_DEFAULT = '#00ff00'
|
||||
const BLACK_FALLBACK = '#000000'
|
||||
|
||||
function createMockNode(): {
|
||||
@@ -34,48 +33,26 @@ function createColorSpec(
|
||||
}
|
||||
|
||||
describe('useColorWidget', () => {
|
||||
it('uses the top-level default produced by the V1 → V2 migration', () => {
|
||||
it('uses the declared default from the input spec', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
const inputSpec = createColorSpec({ default: TOP_LEVEL_DEFAULT })
|
||||
const inputSpec = createColorSpec({ default: DECLARED_DEFAULT })
|
||||
|
||||
const widget = useColorWidget()(node, inputSpec)
|
||||
|
||||
expect(addWidget).toHaveBeenCalledWith(
|
||||
'color',
|
||||
'color',
|
||||
TOP_LEVEL_DEFAULT,
|
||||
DECLARED_DEFAULT,
|
||||
expect.any(Function),
|
||||
{ serialize: true }
|
||||
)
|
||||
expect(widget.value).toBe(TOP_LEVEL_DEFAULT)
|
||||
expect(widget.value).toBe(DECLARED_DEFAULT)
|
||||
})
|
||||
|
||||
it('uses options.default when the spec follows the V2 nested shape', () => {
|
||||
it('falls back to black when no default is supplied', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
const inputSpec = createColorSpec({ options: { default: NESTED_DEFAULT } })
|
||||
|
||||
useColorWidget()(node, inputSpec)
|
||||
|
||||
expect(addWidget.mock.calls[0]![2]).toBe(NESTED_DEFAULT)
|
||||
})
|
||||
|
||||
it('prefers the top-level default when both locations are present', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
const inputSpec = createColorSpec({
|
||||
default: TOP_LEVEL_DEFAULT,
|
||||
options: { default: NESTED_DEFAULT }
|
||||
})
|
||||
|
||||
useColorWidget()(node, inputSpec)
|
||||
|
||||
expect(addWidget.mock.calls[0]![2]).toBe(TOP_LEVEL_DEFAULT)
|
||||
})
|
||||
|
||||
it('still produces a usable widget when no default is supplied', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
const inputSpec = createColorSpec()
|
||||
|
||||
const widget = useColorWidget()(node, inputSpec)
|
||||
const widget = useColorWidget()(node, createColorSpec())
|
||||
|
||||
expect(addWidget).toHaveBeenCalledOnce()
|
||||
expect(widget.type).toBe('color')
|
||||
@@ -86,7 +63,7 @@ describe('useColorWidget', () => {
|
||||
it('serializes the widget so its value persists in saved workflows', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
|
||||
useColorWidget()(node, createColorSpec({ default: TOP_LEVEL_DEFAULT }))
|
||||
useColorWidget()(node, createColorSpec({ default: DECLARED_DEFAULT }))
|
||||
|
||||
expect(addWidget.mock.calls[0]![4]).toEqual({ serialize: true })
|
||||
})
|
||||
@@ -96,7 +73,7 @@ describe('useColorWidget', () => {
|
||||
|
||||
useColorWidget()(
|
||||
node,
|
||||
createColorSpec({ name: 'bg_color', default: TOP_LEVEL_DEFAULT })
|
||||
createColorSpec({ name: 'bg_color', default: DECLARED_DEFAULT })
|
||||
)
|
||||
|
||||
expect(addWidget.mock.calls[0]![1]).toBe('bg_color')
|
||||
|
||||
@@ -8,9 +8,8 @@ import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
export const useColorWidget = (): ComfyWidgetConstructorV2 => {
|
||||
return (node: LGraphNode, inputSpec: InputSpecV2): IColorWidget => {
|
||||
const colorSpec = inputSpec as ColorInputSpec
|
||||
const { name, options } = colorSpec
|
||||
const defaultValue = colorSpec.default ?? options?.default ?? '#000000'
|
||||
const { name, default: defaultValue = '#000000' } =
|
||||
inputSpec as ColorInputSpec
|
||||
|
||||
const widget = node.addWidget('color', name, defaultValue, () => {}, {
|
||||
serialize: true
|
||||
|
||||
@@ -8,20 +8,17 @@ import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
export const useTextareaWidget = (): ComfyWidgetConstructorV2 => {
|
||||
return (node: LGraphNode, inputSpec: InputSpecV2): ITextareaWidget => {
|
||||
const { name, options = {} } = inputSpec as TextareaInputSpec
|
||||
const textareaSpec = inputSpec as TextareaInputSpec
|
||||
const { name, default: defaultValue = '' } = textareaSpec
|
||||
const widgetOptions = {
|
||||
rows: textareaSpec.rows ?? 5,
|
||||
cols: textareaSpec.cols ?? 50
|
||||
}
|
||||
|
||||
const widget = node.addWidget(
|
||||
'textarea',
|
||||
name,
|
||||
options.default || '',
|
||||
() => {},
|
||||
{
|
||||
serialize: true,
|
||||
rows: options.rows || 5,
|
||||
cols: options.cols || 50,
|
||||
...options
|
||||
}
|
||||
) as ITextareaWidget
|
||||
const widget = node.addWidget('textarea', name, defaultValue, () => {}, {
|
||||
serialize: true,
|
||||
...widgetOptions
|
||||
}) as ITextareaWidget
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
@@ -44,11 +44,7 @@ const zColorInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('COLOR'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
default: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
default: z.string().optional()
|
||||
})
|
||||
|
||||
const zImageInputSpec = zBaseInputOptions.extend({
|
||||
@@ -84,11 +80,7 @@ const zMarkdownInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('MARKDOWN'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
content: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
default: z.string().optional()
|
||||
})
|
||||
|
||||
const zChartInputSpec = zBaseInputOptions.extend({
|
||||
@@ -118,13 +110,9 @@ const zTextareaInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('TEXTAREA'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
rows: z.number().optional(),
|
||||
cols: z.number().optional(),
|
||||
default: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
rows: z.number().optional(),
|
||||
cols: z.number().optional(),
|
||||
default: z.string().optional()
|
||||
})
|
||||
|
||||
const zCurvePoint = z.tuple([z.number(), z.number()])
|
||||
|
||||
Reference in New Issue
Block a user