diff --git a/src/lib/litegraph/src/LGraphNode.configure.test.ts b/src/lib/litegraph/src/LGraphNode.configure.test.ts new file mode 100644 index 00000000000..698bed26b45 --- /dev/null +++ b/src/lib/litegraph/src/LGraphNode.configure.test.ts @@ -0,0 +1,88 @@ +import { beforeEach, describe, expect, it } from 'vitest' + +import { LGraphNode } from '@/lib/litegraph/src/litegraph' +import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation' + +// Mirrors JSON.stringify(NaN) === "null" — the real source of null in workflows. +const roundTrip = (v: T): T => JSON.parse(JSON.stringify(v)) + +function serialisedNode( + overrides: Partial> & { + widgets_values?: unknown[] + } = {} +): ISerialisedNode { + return { + id: 1, + type: 'TestNode', + pos: [0, 0], + size: [200, 100], + flags: {}, + order: 0, + mode: 0, + ...overrides + } as ISerialisedNode +} + +describe('LGraphNode.configure numeric widget sanitization', () => { + let node: LGraphNode + + beforeEach(() => { + node = new LGraphNode('TestNode') + }) + + it('preserves default when widgets_values contains null for a number widget', () => { + node.addWidget('number', 'seed', 42, null, {}) + + node.configure(serialisedNode({ widgets_values: roundTrip([NaN]) })) + + expect(node.widgets![0].value).toBe(42) + }) + + it('preserves default when widgets_values contains NaN for a number widget', () => { + node.addWidget('number', 'seed', 42, null, {}) + + node.configure(serialisedNode({ widgets_values: [NaN] })) + + expect(Number.isNaN(node.widgets![0].value)).toBe(false) + expect(node.widgets![0].value).toBe(42) + }) + + it('still applies valid numeric values normally', () => { + node.addWidget('number', 'seed', 42, null, {}) + + node.configure(serialisedNode({ widgets_values: [99999] })) + + expect(node.widgets![0].value).toBe(99999) + }) + + it('preserves default when widgets_values contains null for a gradientslider widget', () => { + node.addWidget('gradientslider', 'denoise', 0.75, null, {}) + + node.configure(serialisedNode({ widgets_values: roundTrip([NaN]) })) + + expect(node.widgets![0].value).toBe(0.75) + }) + + it('does not sanitize null for non-numeric widget types', () => { + // TODO: null from a serialized workflow probably should not clobber text + // widgets either; this test documents the current intentional scope limit. + node.addWidget('text', 'prompt', 'default text', null, {}) + + node.configure(serialisedNode({ widgets_values: [null] })) + + expect(node.widgets![0].value).toBeNull() + }) + + it('preserves correct slot ordering when a non-serialized widget precedes a sanitized numeric widget', () => { + // widget A has serialize:false and must not consume a slot in widgets_values + const widgetA = node.addWidget('text', 'label', 'hello', null, {}) + widgetA.serialize = false + node.addWidget('number', 'seed', 42, null, {}) + + // widgets_values has one entry — for the number widget only + node.configure(serialisedNode({ widgets_values: roundTrip([NaN]) })) + + expect(node.widgets![0].value).toBe('hello') // non-serialized, untouched + expect(node.widgets![1].value).toBe(42) // sanitized, default preserved + }) +}) diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 934239cc7dc..4b5205c29f4 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -93,7 +93,7 @@ import { warnDeprecated } from './utils/feedback' import { distributeSpace } from './utils/spaceDistribution' import { truncateText } from './utils/textUtils' import { BaseWidget } from './widgets/BaseWidget' -import { toConcreteWidget } from './widgets/widgetMap' +import { isNumericWidget, toConcreteWidget } from './widgets/widgetMap' import type { WidgetTypeMap } from './widgets/widgetMap' // #region Types @@ -919,7 +919,12 @@ export class LGraphNode for (const widget of this.widgets ?? []) { if (widget.serialize === false) continue if (i >= info.widgets_values.length) break - widget.value = info.widgets_values[i++] + const incoming = info.widgets_values[i++] + const isInvalid = + incoming == null || + (typeof incoming === 'number' && !Number.isFinite(incoming)) + if (isNumericWidget(widget) && isInvalid) continue + widget.value = incoming } } } diff --git a/src/lib/litegraph/src/widgets/widgetMap.ts b/src/lib/litegraph/src/widgets/widgetMap.ts index 86fe73d9e06..50a3b40f439 100644 --- a/src/lib/litegraph/src/widgets/widgetMap.ts +++ b/src/lib/litegraph/src/widgets/widgetMap.ts @@ -165,4 +165,16 @@ export function isAssetWidget(widget: IBaseWidget): widget is IAssetWidget { return widget.type === 'asset' } +const NUMERIC_WIDGET_TYPES = new Set([ + 'number', + 'slider', + 'gradientslider', + 'knob' +]) + +/** Returns true when the widget's value must be a finite number (rejects null/NaN). */ +export function isNumericWidget(widget: IBaseWidget): boolean { + return NUMERIC_WIDGET_TYPES.has(widget.type) +} + // #endregion Type Guards