mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
fix: scope custom combo copy-paste restore to extension
This commit is contained in:
102
src/extensions/core/customWidgets.clone.test.ts
Normal file
102
src/extensions/core/customWidgets.clone.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
|
||||
const TEST_CUSTOM_COMBO_TYPE = 'test/CustomComboCopyPaste'
|
||||
|
||||
class TestCustomComboNode extends LGraphNode {
|
||||
static override title = 'CustomCombo'
|
||||
|
||||
constructor() {
|
||||
super('CustomCombo')
|
||||
this.serialize_widgets = true
|
||||
this.addOutput('value', '*')
|
||||
this.addWidget('combo', 'value', '', () => {}, {
|
||||
values: [] as string[]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function findWidget(node: LGraphNode, name: string) {
|
||||
return node.widgets?.find((widget) => widget.name === name)
|
||||
}
|
||||
|
||||
function getCustomWidgetsExtension(): ComfyExtension {
|
||||
const extension = useExtensionStore().extensions.find(
|
||||
(candidate) => candidate.name === 'Comfy.CustomWidgets'
|
||||
)
|
||||
|
||||
if (!extension) {
|
||||
throw new Error('Comfy.CustomWidgets extension was not registered')
|
||||
}
|
||||
|
||||
return extension
|
||||
}
|
||||
|
||||
describe('CustomCombo copy/paste', () => {
|
||||
beforeAll(async () => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
await import('./customWidgets')
|
||||
|
||||
const extension = getCustomWidgetsExtension()
|
||||
await extension.beforeRegisterNodeDef?.(
|
||||
TestCustomComboNode,
|
||||
{ name: 'CustomCombo' } as ComfyNodeDef,
|
||||
app
|
||||
)
|
||||
|
||||
if (LiteGraph.registered_node_types[TEST_CUSTOM_COMBO_TYPE]) {
|
||||
LiteGraph.unregisterNodeType(TEST_CUSTOM_COMBO_TYPE)
|
||||
}
|
||||
LiteGraph.registerNodeType(TEST_CUSTOM_COMBO_TYPE, TestCustomComboNode)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (LiteGraph.registered_node_types[TEST_CUSTOM_COMBO_TYPE]) {
|
||||
LiteGraph.unregisterNodeType(TEST_CUSTOM_COMBO_TYPE)
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves combo options and selected value through clone and paste', () => {
|
||||
const graph = new LGraph()
|
||||
const appWithRootGraph = app as typeof app & { rootGraphInternal?: LGraph }
|
||||
const previousRootGraph = appWithRootGraph.rootGraphInternal
|
||||
appWithRootGraph.rootGraphInternal = graph
|
||||
|
||||
try {
|
||||
const original = LiteGraph.createNode(TEST_CUSTOM_COMBO_TYPE)!
|
||||
graph.add(original)
|
||||
|
||||
findWidget(original, 'option1')!.value = 'alpha'
|
||||
findWidget(original, 'option2')!.value = 'beta'
|
||||
findWidget(original, 'option3')!.value = 'gamma'
|
||||
findWidget(original, 'value')!.value = 'beta'
|
||||
|
||||
const clonedSerialised = original.clone()?.serialize()
|
||||
|
||||
expect(clonedSerialised).toBeDefined()
|
||||
|
||||
const pasted = LiteGraph.createNode(TEST_CUSTOM_COMBO_TYPE)!
|
||||
pasted.configure(clonedSerialised!)
|
||||
graph.add(pasted)
|
||||
|
||||
expect(findWidget(pasted, 'value')!.value).toBe('beta')
|
||||
expect(findWidget(pasted, 'option1')!.value).toBe('alpha')
|
||||
expect(findWidget(pasted, 'option2')!.value).toBe('beta')
|
||||
expect(findWidget(pasted, 'option3')!.value).toBe('gamma')
|
||||
expect(findWidget(pasted, 'value')!.options.values).toEqual([
|
||||
'alpha',
|
||||
'beta',
|
||||
'gamma'
|
||||
])
|
||||
} finally {
|
||||
appWithRootGraph.rootGraphInternal = previousRootGraph
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -78,16 +78,17 @@ function onCustomComboCreated(this: LGraphNode) {
|
||||
const widgetName = `option${newCount}`
|
||||
const widget = node.addWidget('string', widgetName, '', () => {})
|
||||
if (!widget) return
|
||||
let localValue = `${widget.value ?? ''}`
|
||||
|
||||
Object.defineProperty(widget, 'value', {
|
||||
get() {
|
||||
return useWidgetValueStore().getWidget(
|
||||
app.rootGraph.id,
|
||||
node.id,
|
||||
widgetName
|
||||
)?.value
|
||||
return (
|
||||
useWidgetValueStore().getWidget(app.rootGraph.id, node.id, widgetName)
|
||||
?.value ?? localValue
|
||||
)
|
||||
},
|
||||
set(v: string) {
|
||||
localValue = v
|
||||
const state = useWidgetValueStore().getWidget(
|
||||
app.rootGraph.id,
|
||||
node.id,
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
describe('Widget options.values serialization', () => {
|
||||
it('preserves combo options.values through serialize/configure round-trip', () => {
|
||||
const graph = new LGraph()
|
||||
|
||||
// Create node with combo + option widgets manually
|
||||
const node = new LGraphNode('TestCombo')
|
||||
node.serialize_widgets = true
|
||||
node.addWidget('combo', 'value', 'alpha', () => {}, {
|
||||
values: ['alpha', 'beta', 'gamma']
|
||||
})
|
||||
node.addWidget('string', 'option1', 'alpha', () => {})
|
||||
node.addWidget('string', 'option2', 'beta', () => {})
|
||||
node.addWidget('string', 'option3', 'gamma', () => {})
|
||||
graph.add(node)
|
||||
|
||||
// Serialize
|
||||
const serialized = node.serialize()
|
||||
|
||||
// Create fresh node and configure
|
||||
const restored = new LGraphNode('TestCombo')
|
||||
restored.serialize_widgets = true
|
||||
const restoredCombo = restored.addWidget('combo', 'value', '', () => {}, {
|
||||
values: [] as string[]
|
||||
})
|
||||
restored.addWidget('string', 'option1', '', () => {})
|
||||
restored.addWidget('string', 'option2', '', () => {})
|
||||
restored.addWidget('string', 'option3', '', () => {})
|
||||
graph.add(restored)
|
||||
|
||||
restored.configure(serialized)
|
||||
|
||||
// Widget values should be restored
|
||||
expect(restored.widgets![0].value).toBe('alpha')
|
||||
expect(restored.widgets![1].value).toBe('alpha')
|
||||
expect(restored.widgets![2].value).toBe('beta')
|
||||
expect(restored.widgets![3].value).toBe('gamma')
|
||||
|
||||
const restoredValues = restoredCombo.options.values as string[]
|
||||
expect(restoredValues).toContain('alpha')
|
||||
expect(restoredValues).toContain('beta')
|
||||
expect(restoredValues).toContain('gamma')
|
||||
})
|
||||
})
|
||||
@@ -920,25 +920,6 @@ export class LGraphNode
|
||||
if (i >= info.widgets_values.length) break
|
||||
widget.value = info.widgets_values[i++]
|
||||
}
|
||||
|
||||
// Rebuild combo options.values from sibling option widgets.
|
||||
// widget.options.values is not serialized, so combo widgets
|
||||
// lose their option list after serialize/configure (e.g. copy-paste).
|
||||
for (const widget of this.widgets ?? []) {
|
||||
if (widget.type !== 'combo') continue
|
||||
const values = widget.options.values
|
||||
if (!Array.isArray(values) || values.length > 0) continue
|
||||
|
||||
const optionValues = (this.widgets ?? [])
|
||||
.filter(
|
||||
(w) =>
|
||||
w.name.startsWith('option') &&
|
||||
w.value !== undefined &&
|
||||
w.value !== ''
|
||||
)
|
||||
.map((w) => `${w.value}`)
|
||||
if (optionValues.length > 0) values.push(...optionValues)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user