mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
feat: widgetValueStore promotion resolution
Add resolvePromotedWidget() to WidgetValueStore for looking up interior widget state by subgraph + nodeId + widgetName. Add getPromotionList() helper as the single entry point for reading the promotion list from a SubgraphNode's properties.proxyWidgets. Phase 1 of ProxyWidget elimination — purely additive, no behavior changes. Amp-Thread-ID: https://ampcode.com/threads/T-019c4fd4-6e4c-721f-8106-7b3f3cb93990 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
44
src/core/graph/subgraph/promotionList.test.ts
Normal file
44
src/core/graph/subgraph/promotionList.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getPromotionList } from './promotionList'
|
||||
|
||||
function mockSubgraphNode(proxyWidgets?: unknown) {
|
||||
return { properties: { proxyWidgets } } as any
|
||||
}
|
||||
|
||||
describe('getPromotionList', () => {
|
||||
it('returns empty array for node with no proxyWidgets property', () => {
|
||||
const node = mockSubgraphNode(undefined)
|
||||
expect(getPromotionList(node)).toEqual([])
|
||||
})
|
||||
|
||||
it('returns empty array for empty proxyWidgets', () => {
|
||||
const node = mockSubgraphNode([])
|
||||
expect(getPromotionList(node)).toEqual([])
|
||||
})
|
||||
|
||||
it('parses valid promotion entries', () => {
|
||||
const entries = [
|
||||
['42', 'seed'],
|
||||
['7', 'steps']
|
||||
]
|
||||
const node = mockSubgraphNode(entries)
|
||||
expect(getPromotionList(node)).toEqual(entries)
|
||||
})
|
||||
|
||||
it('handles string-serialized proxyWidgets (JSON)', () => {
|
||||
const entries = [['42', 'seed']]
|
||||
const node = mockSubgraphNode(JSON.stringify(entries))
|
||||
expect(getPromotionList(node)).toEqual(entries)
|
||||
})
|
||||
|
||||
it('throws on invalid format', () => {
|
||||
const node = mockSubgraphNode('not-valid-json{{{')
|
||||
expect(() => getPromotionList(node)).toThrow()
|
||||
})
|
||||
|
||||
it('throws on structurally invalid data', () => {
|
||||
const node = mockSubgraphNode([['only-one-element']])
|
||||
expect(() => getPromotionList(node)).toThrow()
|
||||
})
|
||||
})
|
||||
15
src/core/graph/subgraph/promotionList.ts
Normal file
15
src/core/graph/subgraph/promotionList.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { ProxyWidgetsProperty } from '@/core/schemas/proxyWidget'
|
||||
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
|
||||
/**
|
||||
* Returns the list of promoted widget entries from a SubgraphNode.
|
||||
* Each entry is a [nodeId, widgetName] tuple referencing an interior widget.
|
||||
*
|
||||
* This is the single entry point for reading the promotion list,
|
||||
* replacing direct access to `node.properties.proxyWidgets`.
|
||||
*/
|
||||
export function getPromotionList(node: SubgraphNode): ProxyWidgetsProperty {
|
||||
if (node.properties.proxyWidgets == null) return []
|
||||
return parseProxyWidgets(node.properties.proxyWidgets)
|
||||
}
|
||||
@@ -2,8 +2,12 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
import type { WidgetState } from './widgetValueStore'
|
||||
import { useWidgetValueStore } from './widgetValueStore'
|
||||
import { stripGraphPrefix, useWidgetValueStore } from './widgetValueStore'
|
||||
|
||||
function widget<T>(
|
||||
nodeId: string,
|
||||
@@ -17,6 +21,22 @@ function widget<T>(
|
||||
return { nodeId, name, type, value, options: {}, ...extra }
|
||||
}
|
||||
|
||||
function mockWidget(name: string, type = 'number'): IBaseWidget {
|
||||
return { name, type } as IBaseWidget
|
||||
}
|
||||
|
||||
function mockNode(id: string, widgets: IBaseWidget[] = []): LGraphNode {
|
||||
return { id, widgets } as unknown as LGraphNode
|
||||
}
|
||||
|
||||
function mockSubgraph(nodes: LGraphNode[]): LGraph {
|
||||
const nodeMap = new Map(nodes.map((n) => [String(n.id), n]))
|
||||
return {
|
||||
getNodeById: (id: string | number | null | undefined) =>
|
||||
id != null ? (nodeMap.get(String(id)) ?? null) : null
|
||||
} as unknown as LGraph
|
||||
}
|
||||
|
||||
describe('useWidgetValueStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
@@ -152,4 +172,93 @@ describe('useWidgetValueStore', () => {
|
||||
expect(store.getWidget('node-1', 'seed')?.label).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolvePromotedWidget', () => {
|
||||
it('returns null for missing node', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const subgraph = mockSubgraph([])
|
||||
|
||||
expect(store.resolvePromotedWidget(subgraph, '99', 'seed')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for missing widget on existing node', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const node = mockNode('42', [mockWidget('steps')])
|
||||
const subgraph = mockSubgraph([node])
|
||||
store.registerWidget(widget('42', 'steps', 'number', 20))
|
||||
|
||||
expect(store.resolvePromotedWidget(subgraph, '42', 'seed')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when widget exists on node but not in store', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const w = mockWidget('seed')
|
||||
const node = mockNode('42', [w])
|
||||
const subgraph = mockSubgraph([node])
|
||||
|
||||
expect(store.resolvePromotedWidget(subgraph, '42', 'seed')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns correct state, widget, and node for registered widget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const w = mockWidget('seed')
|
||||
const node = mockNode('42', [w])
|
||||
const subgraph = mockSubgraph([node])
|
||||
const registeredState = store.registerWidget(
|
||||
widget('42', 'seed', 'number', 12345)
|
||||
)
|
||||
|
||||
const result = store.resolvePromotedWidget(subgraph, '42', 'seed')
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.state).toBe(registeredState)
|
||||
expect(result!.widget).toBe(w)
|
||||
expect(result!.node).toBe(node)
|
||||
})
|
||||
|
||||
it('state.value matches the store value (same reference)', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const w = mockWidget('seed')
|
||||
const node = mockNode('42', [w])
|
||||
const subgraph = mockSubgraph([node])
|
||||
store.registerWidget(widget('42', 'seed', 'number', 100))
|
||||
|
||||
const result = store.resolvePromotedWidget(subgraph, '42', 'seed')
|
||||
expect(result!.state.value).toBe(100)
|
||||
|
||||
result!.state.value = 200
|
||||
expect(store.getWidget('42', 'seed')?.value).toBe(200)
|
||||
})
|
||||
|
||||
it('handles stripGraphPrefix for scoped node IDs', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const w = mockWidget('cfg')
|
||||
const node = mockNode('7', [w])
|
||||
const subgraph = mockSubgraph([node])
|
||||
store.registerWidget(widget('7', 'cfg', 'number', 7.5))
|
||||
|
||||
// nodeId passed as bare '7' resolves to store key '7:cfg'
|
||||
const result = store.resolvePromotedWidget(subgraph, '7', 'cfg')
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.state.value).toBe(7.5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('stripGraphPrefix', () => {
|
||||
it('strips single prefix', () => {
|
||||
expect(stripGraphPrefix('graph1:42')).toBe('42')
|
||||
})
|
||||
|
||||
it('strips multiple prefixes', () => {
|
||||
expect(stripGraphPrefix('graph1:subgraph2:42')).toBe('42')
|
||||
})
|
||||
|
||||
it('returns bare id unchanged', () => {
|
||||
expect(stripGraphPrefix('42')).toBe('42')
|
||||
})
|
||||
|
||||
it('handles numeric input', () => {
|
||||
expect(stripGraphPrefix(42 as unknown as string)).toBe('42')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IWidgetOptions
|
||||
@@ -69,9 +70,27 @@ export const useWidgetValueStore = defineStore('widgetValue', () => {
|
||||
return widgetStates.value.get(makeKey(nodeId, widgetName))
|
||||
}
|
||||
|
||||
function resolvePromotedWidget(
|
||||
subgraph: LGraph,
|
||||
nodeId: NodeId,
|
||||
widgetName: string
|
||||
): { state: WidgetState; widget: IBaseWidget; node: LGraphNode } | null {
|
||||
const node = subgraph.getNodeById(nodeId)
|
||||
if (!node) return null
|
||||
|
||||
const widget = node.widgets?.find((w) => w.name === widgetName)
|
||||
if (!widget) return null
|
||||
|
||||
const state = getWidget(stripGraphPrefix(nodeId), widgetName)
|
||||
if (!state) return null
|
||||
|
||||
return { state, widget, node }
|
||||
}
|
||||
|
||||
return {
|
||||
registerWidget,
|
||||
getWidget,
|
||||
getNodeWidgets
|
||||
getNodeWidgets,
|
||||
resolvePromotedWidget
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user