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:
Glary-Bot
2026-05-22 04:04:50 +00:00
parent 86b371c273
commit 604656fb28
4 changed files with 27 additions and 66 deletions

View File

@@ -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')

View File

@@ -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

View File

@@ -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
}

View File

@@ -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()])