mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-11 01:28:03 +00:00
Extract WidgetGridItem and WidgetSlotMetadata into a shared types module and make ProcessedWidget extend it, dropping the dead flat fields it duplicated. Decompose computeProcessedWidgets into resolveLiveWidgetContext + processWidget, dedupe ids via a Set, and stop exporting isWidgetVisible/hasWidgetError (tested through the public compute path instead). WidgetGrid drives widgets through :model-value so updateHandler is the sole writer; preview items stay static. removeWidget now drops a widget from the render order without purging its stored value, so remove-then-re-add restores what the user set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
281 lines
9.3 KiB
TypeScript
281 lines
9.3 KiB
TypeScript
import { createTestingPinia } from '@pinia/testing'
|
|
import { setActivePinia } from 'pinia'
|
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import type { UUID } from '@/utils/uuid'
|
|
import { toNodeId } from '@/types/nodeId'
|
|
import { widgetId } from '@/types/widgetId'
|
|
import type { WidgetState } from '@/types/widgetState'
|
|
|
|
import { useWidgetValueStore } from './widgetValueStore'
|
|
|
|
function state<T>(
|
|
type: string,
|
|
value: T,
|
|
extra: Partial<Omit<WidgetState<T>, 'type' | 'value'>> = {}
|
|
): Omit<WidgetState<T>, 'nodeId' | 'name' | 'y'> & { y?: number } {
|
|
return { type, value, options: {}, ...extra }
|
|
}
|
|
|
|
describe('useWidgetValueStore', () => {
|
|
const graphA = 'graph-a' as UUID
|
|
const graphB = 'graph-b' as UUID
|
|
const seedA = widgetId(graphA, toNodeId('node-1'), 'seed')
|
|
const seedB = widgetId(graphB, toNodeId('node-1'), 'seed')
|
|
|
|
beforeEach(() => {
|
|
setActivePinia(createTestingPinia({ stubActions: false }))
|
|
})
|
|
|
|
describe('widgetState.value access', () => {
|
|
it('getWidget returns undefined for unregistered widget', () => {
|
|
const store = useWidgetValueStore()
|
|
expect(store.getWidget(seedA)).toBeUndefined()
|
|
})
|
|
|
|
it('widgetState.value can be read and written directly', () => {
|
|
const store = useWidgetValueStore()
|
|
const registered = store.registerWidget(seedA, state('number', 100))
|
|
expect(registered.value).toBe(100)
|
|
|
|
registered.value = 200
|
|
expect(store.getWidget(seedA)?.value).toBe(200)
|
|
})
|
|
|
|
it('stores different value types', () => {
|
|
const store = useWidgetValueStore()
|
|
store.registerWidget(
|
|
widgetId(graphA, toNodeId('node-1'), 'text'),
|
|
state('string', 'hello')
|
|
)
|
|
store.registerWidget(
|
|
widgetId(graphA, toNodeId('node-1'), 'number'),
|
|
state('number', 42)
|
|
)
|
|
store.registerWidget(
|
|
widgetId(graphA, toNodeId('node-1'), 'boolean'),
|
|
state('toggle', true)
|
|
)
|
|
store.registerWidget(
|
|
widgetId(graphA, toNodeId('node-1'), 'array'),
|
|
state('combo', [1, 2, 3])
|
|
)
|
|
|
|
expect(
|
|
store.getWidget(widgetId(graphA, toNodeId('node-1'), 'text'))?.value
|
|
).toBe('hello')
|
|
expect(
|
|
store.getWidget(widgetId(graphA, toNodeId('node-1'), 'number'))?.value
|
|
).toBe(42)
|
|
expect(
|
|
store.getWidget(widgetId(graphA, toNodeId('node-1'), 'boolean'))?.value
|
|
).toBe(true)
|
|
expect(
|
|
store.getWidget(widgetId(graphA, toNodeId('node-1'), 'array'))?.value
|
|
).toEqual([1, 2, 3])
|
|
})
|
|
})
|
|
|
|
describe('widget registration', () => {
|
|
it('registers a widget with minimal properties', () => {
|
|
const store = useWidgetValueStore()
|
|
const registered = store.registerWidget(seedA, state('number', 12345))
|
|
|
|
expect(registered.nodeId).toBe('node-1')
|
|
expect(registered.name).toBe('seed')
|
|
expect(registered.type).toBe('number')
|
|
expect(registered.value).toBe(12345)
|
|
expect(registered.disabled).toBeUndefined()
|
|
expect(registered.serialize).toBeUndefined()
|
|
expect(registered.options).toEqual({})
|
|
expect(registered.y).toBe(0)
|
|
})
|
|
|
|
it('registers explicit widget layout y', () => {
|
|
const store = useWidgetValueStore()
|
|
const registered = store.registerWidget(
|
|
seedA,
|
|
state('number', 12345, { y: 42 })
|
|
)
|
|
|
|
expect(registered.y).toBe(42)
|
|
})
|
|
|
|
it('registerWidget is idempotent and does not overwrite existing state', () => {
|
|
const store = useWidgetValueStore()
|
|
const first = store.registerWidget(seedA, state('number', 11))
|
|
first.value = 99
|
|
|
|
const second = store.registerWidget(seedA, state('number', 11))
|
|
expect(second).toBe(first)
|
|
expect(second.value).toBe(99)
|
|
})
|
|
|
|
it('registers a widget with all properties', () => {
|
|
const store = useWidgetValueStore()
|
|
const registered = store.registerWidget(
|
|
seedA,
|
|
state('string', 'test', {
|
|
label: 'Prompt Text',
|
|
disabled: true,
|
|
serialize: false,
|
|
options: { multiline: true }
|
|
})
|
|
)
|
|
|
|
expect(registered.label).toBe('Prompt Text')
|
|
expect(registered.disabled).toBe(true)
|
|
expect(registered.serialize).toBe(false)
|
|
expect(registered.options).toEqual({ multiline: true })
|
|
})
|
|
})
|
|
|
|
describe('widget getters', () => {
|
|
it('getWidget returns widget state', () => {
|
|
const store = useWidgetValueStore()
|
|
store.registerWidget(seedA, state('number', 100))
|
|
|
|
const registered = store.getWidget(seedA)
|
|
expect(registered).toBeDefined()
|
|
expect(registered?.name).toBe('seed')
|
|
expect(registered?.value).toBe(100)
|
|
})
|
|
|
|
it('getNodeWidgets returns widgets in registration order', () => {
|
|
const store = useWidgetValueStore()
|
|
store.registerWidget(
|
|
widgetId(graphA, toNodeId('node-1'), 'seed'),
|
|
state('number', 1)
|
|
)
|
|
store.registerWidget(
|
|
widgetId(graphA, toNodeId('node-1'), 'steps'),
|
|
state('number', 20)
|
|
)
|
|
store.registerWidget(
|
|
widgetId(graphA, toNodeId('node-2'), 'cfg'),
|
|
state('number', 7)
|
|
)
|
|
|
|
const widgets = store.getNodeWidgets(graphA, toNodeId('node-1'))
|
|
expect(widgets.map((w) => w.name)).toEqual(['seed', 'steps'])
|
|
})
|
|
|
|
it('getNodeWidgetIds returns the explicit node widget order', () => {
|
|
const store = useWidgetValueStore()
|
|
const seed = widgetId(graphA, toNodeId('node-1'), 'seed')
|
|
const steps = widgetId(graphA, toNodeId('node-1'), 'steps')
|
|
const cfg = widgetId(graphA, toNodeId('node-1'), 'cfg')
|
|
store.registerWidget(seed, state('number', 1))
|
|
store.registerWidget(steps, state('number', 20))
|
|
store.registerWidget(cfg, state('number', 7))
|
|
|
|
store.setNodeWidgetOrder(graphA, toNodeId('node-1'), [cfg, seed])
|
|
|
|
expect(store.getNodeWidgetIds(graphA, toNodeId('node-1'))).toEqual([
|
|
cfg,
|
|
seed,
|
|
steps
|
|
])
|
|
expect(
|
|
store.getNodeWidgets(graphA, toNodeId('node-1')).map((w) => w.name)
|
|
).toEqual(['cfg', 'seed', 'steps'])
|
|
})
|
|
|
|
it('ignores widget IDs from other nodes when setting order', () => {
|
|
const store = useWidgetValueStore()
|
|
const seed = widgetId(graphA, toNodeId('node-1'), 'seed')
|
|
const other = widgetId(graphA, toNodeId('node-2'), 'cfg')
|
|
store.registerWidget(seed, state('number', 1))
|
|
store.registerWidget(other, state('number', 7))
|
|
|
|
store.setNodeWidgetOrder(graphA, toNodeId('node-1'), [other, seed])
|
|
|
|
expect(store.getNodeWidgetIds(graphA, toNodeId('node-1'))).toEqual([seed])
|
|
})
|
|
})
|
|
|
|
describe('value mutation', () => {
|
|
it('setValue updates registered widgets and reports missing widgets', () => {
|
|
const store = useWidgetValueStore()
|
|
store.registerWidget(seedA, state('number', 100))
|
|
|
|
expect(store.setValue(seedA, 200)).toBe(true)
|
|
expect(store.getWidget(seedA)?.value).toBe(200)
|
|
expect(
|
|
store.setValue(widgetId(graphA, toNodeId('missing'), 'seed'), 1)
|
|
).toBe(false)
|
|
})
|
|
|
|
it('deleteWidget removes registered widgets from node order', () => {
|
|
const store = useWidgetValueStore()
|
|
const steps = widgetId(graphA, toNodeId('node-1'), 'steps')
|
|
store.registerWidget(seedA, state('number', 100))
|
|
store.registerWidget(steps, state('number', 20))
|
|
|
|
expect(store.deleteWidget(seedA)).toBe(true)
|
|
expect(store.getWidget(seedA)).toBeUndefined()
|
|
expect(store.getNodeWidgetIds(graphA, toNodeId('node-1'))).toEqual([
|
|
steps
|
|
])
|
|
expect(store.deleteWidget(seedA)).toBe(false)
|
|
})
|
|
|
|
it('removeNodeWidgetOrder drops the id from order but keeps its value', () => {
|
|
const store = useWidgetValueStore()
|
|
const steps = widgetId(graphA, toNodeId('node-1'), 'steps')
|
|
store.registerWidget(seedA, state('number', 100))
|
|
store.registerWidget(steps, state('number', 20))
|
|
|
|
store.removeNodeWidgetOrder(seedA)
|
|
|
|
expect(store.getNodeWidgetIds(graphA, toNodeId('node-1'))).toEqual([
|
|
steps
|
|
])
|
|
expect(store.getWidget(seedA)?.value).toBe(100)
|
|
})
|
|
})
|
|
|
|
describe('direct property mutation', () => {
|
|
it('disabled can be set directly via getWidget', () => {
|
|
const store = useWidgetValueStore()
|
|
const registered = store.registerWidget(seedA, state('number', 100))
|
|
|
|
registered.disabled = true
|
|
expect(store.getWidget(seedA)?.disabled).toBe(true)
|
|
})
|
|
|
|
it('label can be set directly via getWidget', () => {
|
|
const store = useWidgetValueStore()
|
|
const registered = store.registerWidget(seedA, state('number', 100))
|
|
|
|
registered.label = 'Random Seed'
|
|
expect(store.getWidget(seedA)?.label).toBe('Random Seed')
|
|
|
|
registered.label = undefined
|
|
expect(store.getWidget(seedA)?.label).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('graph isolation', () => {
|
|
it('isolates widget states by graph', () => {
|
|
const store = useWidgetValueStore()
|
|
store.registerWidget(seedA, state('number', 1))
|
|
store.registerWidget(seedB, state('number', 2))
|
|
|
|
expect(store.getWidget(seedA)?.value).toBe(1)
|
|
expect(store.getWidget(seedB)?.value).toBe(2)
|
|
})
|
|
|
|
it('clearGraph only removes one graph namespace', () => {
|
|
const store = useWidgetValueStore()
|
|
store.registerWidget(seedA, state('number', 1))
|
|
store.registerWidget(seedB, state('number', 2))
|
|
|
|
store.clearGraph(graphA)
|
|
|
|
expect(store.getWidget(seedA)).toBeUndefined()
|
|
expect(store.getWidget(seedB)?.value).toBe(2)
|
|
})
|
|
})
|
|
})
|