Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions src/composables/graph/useGraphNodeManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { computed, nextTick, watch } from 'vue'

import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import { createPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetView'

Check failure on line 7 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / scan

Cannot find module '@/core/graph/subgraph/promotedWidgetView' or its corresponding type declarations.

Check failure on line 7 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / setup

Cannot find module '@/core/graph/subgraph/promotedWidgetView' or its corresponding type declarations.

Check failure on line 7 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

Unable to resolve path to module '@/core/graph/subgraph/promotedWidgetView'
import { BaseWidget, LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
Expand Down Expand Up @@ -74,3 +75,193 @@
expect(widgetValue.value).toBe(99)
})
})

describe('Widget slotMetadata reactivity on link disconnect', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})

function createWidgetInputGraph() {
const graph = new LGraph()
const node = new LGraphNode('test')

// Add a widget and an associated input slot (simulates "widget converted to input")
node.addWidget('string', 'prompt', 'hello', () => undefined, {})
const input = node.addInput('prompt', 'STRING')
// Associate the input slot with the widget (as widgetInputs extension does)
input.widget = { name: 'prompt' }

// Start with a connected link
input.link = 42

graph.add(node)
return { graph, node }
}

it('sets slotMetadata.linked to true when input has a link', () => {
const { graph, node } = createWidgetInputGraph()
const { vueNodeData } = useGraphNodeManager(graph)

const nodeData = vueNodeData.get(String(node.id))
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')

expect(widgetData?.slotMetadata).toBeDefined()
expect(widgetData?.slotMetadata?.linked).toBe(true)
})

it('updates slotMetadata.linked to false after link disconnect event', async () => {
const { graph, node } = createWidgetInputGraph()
const { vueNodeData } = useGraphNodeManager(graph)

const nodeData = vueNodeData.get(String(node.id))
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')

// Verify initially linked
expect(widgetData?.slotMetadata?.linked).toBe(true)

// Simulate link disconnection (as LiteGraph does before firing the event)
node.inputs[0].link = null

// Fire the trigger event that LiteGraph fires on disconnect
graph.trigger('node:slot-links:changed', {
nodeId: node.id,
slotType: NodeSlotType.INPUT,
slotIndex: 0,
connected: false,
linkId: 42
})

await nextTick()

// slotMetadata.linked should now be false
expect(widgetData?.slotMetadata?.linked).toBe(false)
})

it('reactively updates disabled state in a derived computed after disconnect', async () => {
const { graph, node } = createWidgetInputGraph()
const { vueNodeData } = useGraphNodeManager(graph)

const nodeData = vueNodeData.get(String(node.id))!

// Mimic what processedWidgets does in NodeWidgets.vue:
// derive disabled from slotMetadata.linked
const derivedDisabled = computed(() => {
const widgets = nodeData.widgets ?? []
const widget = widgets.find((w) => w.name === 'prompt')
return widget?.slotMetadata?.linked ? true : false
})

// Initially linked → disabled
expect(derivedDisabled.value).toBe(true)

// Track changes
const onChange = vi.fn()
watch(derivedDisabled, onChange)

// Simulate disconnect
node.inputs[0].link = null
graph.trigger('node:slot-links:changed', {
nodeId: node.id,
slotType: NodeSlotType.INPUT,
slotIndex: 0,
connected: false,
linkId: 42
})

await nextTick()

// The derived computed should now return false
expect(derivedDisabled.value).toBe(false)
expect(onChange).toHaveBeenCalledTimes(1)
})

it('updates slotMetadata for promoted widgets where SafeWidgetData.name differs from input.widget.name', async () => {
// Set up a subgraph with an interior node that has a "prompt" widget.
// createPromotedWidgetView resolves against this interior node.
const subgraph = createTestSubgraph()

Check failure on line 181 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / scan

Cannot find name 'createTestSubgraph'.

Check failure on line 181 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / setup

Cannot find name 'createTestSubgraph'.
const interiorNode = new LGraphNode('interior')
interiorNode.id = 10
interiorNode.addWidget('string', 'prompt', 'hello', () => undefined, {})
subgraph.add(interiorNode)

const subgraphNode = createTestSubgraphNode(subgraph, { id: 123 })

Check failure on line 187 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / scan

Cannot find name 'createTestSubgraphNode'.

Check failure on line 187 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / setup

Cannot find name 'createTestSubgraphNode'.

// Create a PromotedWidgetView with displayName="value" (subgraph input
// slot name) and sourceWidgetName="prompt" (interior widget name).
// PromotedWidgetView.name returns "value", but safeWidgetMapper sets
// SafeWidgetData.name to sourceWidgetName ("prompt").
const promotedView = createPromotedWidgetView(
subgraphNode,
'10',
'prompt',
'value'
)

// Host the promoted view on a regular node so we can control widgets
// directly (SubgraphNode.widgets is a synthetic getter).
const graph = new LGraph()
const hostNode = new LGraphNode('host')
hostNode.widgets = [promotedView]
const input = hostNode.addInput('value', 'STRING')
input.widget = { name: 'value' }
input.link = 42
graph.add(hostNode)

const { vueNodeData } = useGraphNodeManager(graph)
const nodeData = vueNodeData.get(String(hostNode.id))

// SafeWidgetData.name is "prompt" (sourceWidgetName), but the
// input slot widget name is "value" — slotName bridges this gap.
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
expect(widgetData).toBeDefined()
expect(widgetData?.slotName).toBe('value')
expect(widgetData?.slotMetadata?.linked).toBe(true)

// Disconnect
hostNode.inputs[0].link = null
graph.trigger('node:slot-links:changed', {
nodeId: hostNode.id,
slotType: NodeSlotType.INPUT,
slotIndex: 0,
connected: false,
linkId: 42
})

await nextTick()

expect(widgetData?.slotMetadata?.linked).toBe(false)
})
})

describe('Subgraph Promoted Pseudo Widgets', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})

it('marks promoted $$ widgets as canvasOnly for Vue widget rendering', () => {
const subgraph = createTestSubgraph()

Check failure on line 242 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / scan

Cannot find name 'createTestSubgraph'.

Check failure on line 242 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / setup

Cannot find name 'createTestSubgraph'.
const interiorNode = new LGraphNode('interior')
interiorNode.id = 10
subgraph.add(interiorNode)

const subgraphNode = createTestSubgraphNode(subgraph, { id: 123 })

Check failure on line 247 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / scan

Cannot find name 'createTestSubgraphNode'.

Check failure on line 247 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / setup

Cannot find name 'createTestSubgraphNode'.
const graph = subgraphNode.graph as LGraph
graph.add(subgraphNode)

usePromotionStore().promote(

Check failure on line 251 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / scan

Cannot find name 'usePromotionStore'.

Check failure on line 251 in src/composables/graph/useGraphNodeManager.test.ts

View workflow job for this annotation

GitHub Actions / setup

Cannot find name 'usePromotionStore'.
subgraphNode.rootGraph.id,
subgraphNode.id,
'10',
'$$canvas-image-preview'
)

const { vueNodeData } = useGraphNodeManager(graph)
const vueNode = vueNodeData.get(String(subgraphNode.id))
const promotedWidget = vueNode?.widgets?.find(
(widget) => widget.name === '$$canvas-image-preview'
)

expect(promotedWidget).toBeDefined()
expect(promotedWidget?.options?.canvasOnly).toBe(true)
})
})
17 changes: 13 additions & 4 deletions src/composables/graph/useGraphNodeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@
spec?: InputSpec
/** Input slot metadata (index and link status) */
slotMetadata?: WidgetSlotMetadata
/**
* Original LiteGraph widget name used for slot metadata matching.
* For promoted widgets, `name` is `sourceWidgetName` (interior widget name)
* which differs from the subgraph node's input slot widget name.
*/
slotName?: string
}

export interface VueNodeData {
Expand Down Expand Up @@ -226,9 +232,12 @@
...sharedEnhancements,
callback,
hasLayoutSize: typeof widget.computeLayoutSize === 'function',
isDOMWidget: isDOMWidget(widget),
options,
slotMetadata: slotInfo
isDOMWidget: isDOMWidget(widget) || isPromotedDOMWidget(widget),

Check failure on line 235 in src/composables/graph/useGraphNodeManager.ts

View workflow job for this annotation

GitHub Actions / scan

Cannot find name 'isPromotedDOMWidget'.

Check failure on line 235 in src/composables/graph/useGraphNodeManager.ts

View workflow job for this annotation

GitHub Actions / setup

Cannot find name 'isPromotedDOMWidget'.
options: isPromotedPseudoWidget

Check failure on line 236 in src/composables/graph/useGraphNodeManager.ts

View workflow job for this annotation

GitHub Actions / scan

Cannot find name 'isPromotedPseudoWidget'.

Check failure on line 236 in src/composables/graph/useGraphNodeManager.ts

View workflow job for this annotation

GitHub Actions / setup

Cannot find name 'isPromotedPseudoWidget'.
? { ...options, canvasOnly: true }
: options,
slotMetadata: slotInfo,
slotName: name !== widget.name ? widget.name : undefined
}
} catch (error) {
return {
Expand Down Expand Up @@ -341,7 +350,7 @@

// Update only widgets with new slot metadata, keeping other widget data intact
for (const widget of currentData.widgets ?? []) {
const slotInfo = slotMetadata.get(widget.name)
const slotInfo = slotMetadata.get(widget.slotName ?? widget.name)
if (slotInfo) widget.slotMetadata = slotInfo
}
}
Expand Down
Loading