test: add unit tests for subgraph IO slot link events

- SubgraphInput.connect() fires node:slot-links:changed for widget inputs
- SubgraphInputNode._disconnectNodeInput() fires on disconnect
- LinkConnector.dropOnNothing() dispatches before-drop-on-canvas before
  dropped-on-canvas, and skips downstream when intercepted
- Remove duplicate raf.flush() in finishInteraction
This commit is contained in:
Arthur R Longbottom
2026-02-26 21:35:37 -08:00
committed by Christian Byrne
parent 0d657402e7
commit 180fed69d8
3 changed files with 189 additions and 2 deletions

View File

@@ -0,0 +1,87 @@
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { LinkConnector } from '@/lib/litegraph/src/litegraph'
import {
createMockCanvasPointerEvent,
createMockLGraphNode,
createMockLinkNetwork,
createMockNodeOutputSlot
} from '@/utils/__tests__/litegraphTestUtils'
const mockSetConnectingLinks = vi.fn()
type RenderLinkItem = LinkConnector['renderLinks'][number]
function createMockRenderLink(): RenderLinkItem {
const partial: Partial<RenderLinkItem> = {
toType: 'input',
fromPos: [0, 0],
fromSlotIndex: 0,
fromDirection: 0,
network: createMockLinkNetwork(),
node: createMockLGraphNode(),
fromSlot: createMockNodeOutputSlot(),
dragDirection: 0,
canConnectToInput: vi.fn().mockReturnValue(false),
canConnectToOutput: vi.fn().mockReturnValue(false),
canConnectToReroute: vi.fn().mockReturnValue(false),
connectToInput: vi.fn(),
connectToOutput: vi.fn(),
connectToSubgraphInput: vi.fn(),
connectToRerouteOutput: vi.fn(),
connectToSubgraphOutput: vi.fn(),
connectToRerouteInput: vi.fn()
}
return partial as RenderLinkItem
}
describe('LinkConnector.dropOnNothing event dispatch', () => {
let connector: LinkConnector
beforeEach(() => {
connector = new LinkConnector(mockSetConnectingLinks)
vi.clearAllMocks()
})
test('dispatches before-drop-on-canvas before dropped-on-canvas', () => {
connector.renderLinks.push(createMockRenderLink())
const callOrder: string[] = []
connector.events.addEventListener('before-drop-on-canvas', () => {
callOrder.push('before-drop-on-canvas')
})
connector.events.addEventListener('dropped-on-canvas', () => {
callOrder.push('dropped-on-canvas')
})
connector.dropOnNothing(createMockCanvasPointerEvent(100, 100))
expect(callOrder).toEqual(['before-drop-on-canvas', 'dropped-on-canvas'])
})
test('skips dropped-on-canvas when before-drop-on-canvas is intercepted', () => {
connector.renderLinks.push(createMockRenderLink())
const droppedListener = vi.fn()
connector.events.addEventListener('before-drop-on-canvas', (e) => {
e.preventDefault()
})
connector.events.addEventListener('dropped-on-canvas', droppedListener)
connector.dropOnNothing(createMockCanvasPointerEvent(100, 100))
expect(droppedListener).not.toHaveBeenCalled()
})
test('does not dispatch events when renderLinks is empty', () => {
const beforeListener = vi.fn()
const droppedListener = vi.fn()
connector.events.addEventListener('before-drop-on-canvas', beforeListener)
connector.events.addEventListener('dropped-on-canvas', droppedListener)
connector.dropOnNothing(createMockCanvasPointerEvent(100, 100))
expect(beforeListener).not.toHaveBeenCalled()
expect(droppedListener).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,102 @@
import { describe, expect, vi } from 'vitest'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
import { subgraphTest } from './__fixtures__/subgraphFixtures'
describe('SubgraphInput.connect triggers node:slot-links:changed', () => {
subgraphTest(
'fires connected event when connecting to a widget input',
({ simpleSubgraph }) => {
const subgraph = simpleSubgraph
const triggerSpy = vi.spyOn(subgraph, 'trigger')
const node = new LGraphNode('Target')
node.addInput('prompt', 'STRING')
node.inputs[0].widget = { name: 'prompt' }
subgraph.add(node)
subgraph.inputNode.slots[0].connect(node.inputs[0], node)
expect(triggerSpy).toHaveBeenCalledWith('node:slot-links:changed', {
nodeId: node.id,
slotType: NodeSlotType.INPUT,
slotIndex: 0,
connected: true,
linkId: expect.any(Number)
})
}
)
subgraphTest(
'does not fire event when connecting to a non-widget input',
({ simpleSubgraph }) => {
const subgraph = simpleSubgraph
const triggerSpy = vi.spyOn(subgraph, 'trigger')
const node = new LGraphNode('Target')
node.addInput('in', 'number')
subgraph.add(node)
subgraph.inputNode.slots[0].connect(node.inputs[0], node)
expect(triggerSpy).not.toHaveBeenCalledWith(
'node:slot-links:changed',
expect.anything()
)
}
)
})
describe('SubgraphInputNode._disconnectNodeInput triggers node:slot-links:changed', () => {
subgraphTest(
'fires disconnected event when disconnecting a widget input',
({ simpleSubgraph }) => {
const subgraph = simpleSubgraph
const node = new LGraphNode('Target')
node.addInput('prompt', 'STRING')
node.inputs[0].widget = { name: 'prompt' }
subgraph.add(node)
const link = subgraph.inputNode.slots[0].connect(node.inputs[0], node)
expect(link).toBeDefined()
const triggerSpy = vi.spyOn(subgraph, 'trigger')
subgraph.inputNode._disconnectNodeInput(node, node.inputs[0], link!)
expect(triggerSpy).toHaveBeenCalledWith('node:slot-links:changed', {
nodeId: node.id,
slotType: NodeSlotType.INPUT,
slotIndex: 0,
connected: false,
linkId: link!.id
})
}
)
subgraphTest(
'does not fire event when disconnecting a non-widget input',
({ simpleSubgraph }) => {
const subgraph = simpleSubgraph
const node = new LGraphNode('Target')
node.addInput('in', 'number')
subgraph.add(node)
const link = subgraph.inputNode.slots[0].connect(node.inputs[0], node)
expect(link).toBeDefined()
const triggerSpy = vi.spyOn(subgraph, 'trigger')
subgraph.inputNode._disconnectNodeInput(node, node.inputs[0], link!)
expect(triggerSpy).not.toHaveBeenCalledWith(
'node:slot-links:changed',
expect.anything()
)
}
)
})

View File

@@ -544,8 +544,6 @@ export function useSlotLinkInteraction({
raf.flush()
raf.flush()
if (!state.source) {
cleanupInteraction()
app.canvas?.setDirty(true, true)