mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-18 22:10:03 +00:00
- Remove outdated legacy manager detection from LoadWorkflowWarning - Update InfoPanelHeader with conflict detection improvements - Fix all failing unit tests from state management transition - Clean up algolia search provider type mappings - Remove unused @ts-expect-error directives - Add .nx to .gitignore 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1277 lines
39 KiB
TypeScript
1277 lines
39 KiB
TypeScript
import { afterEach, describe, expect, vi } from 'vitest'
|
|
|
|
import {
|
|
LGraph,
|
|
LGraphNode,
|
|
LLink,
|
|
Reroute,
|
|
type RerouteId
|
|
} from '@/lib/litegraph/src/litegraph'
|
|
import { LinkConnector } from '@/lib/litegraph/src/litegraph'
|
|
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
|
|
|
import { test as baseTest } from './testExtensions'
|
|
|
|
interface TestContext {
|
|
graph: LGraph
|
|
connector: LinkConnector
|
|
setConnectingLinks: ReturnType<typeof vi.fn>
|
|
createTestNode: (id: number) => LGraphNode
|
|
reroutesBeforeTest: [rerouteId: RerouteId, reroute: Reroute][]
|
|
validateIntegrityNoChanges: () => void
|
|
validateIntegrityFloatingRemoved: () => void
|
|
validateLinkIntegrity: () => void
|
|
getNextLinkIds: (
|
|
linkIds: Set<number>,
|
|
expectedExtraLinks?: number
|
|
) => number[]
|
|
readonly floatingReroute: Reroute
|
|
}
|
|
|
|
const test = baseTest.extend<TestContext>({
|
|
reroutesBeforeTest: async ({ reroutesComplexGraph }, use) => {
|
|
await use([...reroutesComplexGraph.reroutes])
|
|
},
|
|
|
|
graph: async ({ reroutesComplexGraph }, use) => {
|
|
const ctx = vi.fn(() => ({ measureText: vi.fn(() => ({ width: 10 })) }))
|
|
for (const node of reroutesComplexGraph.nodes) {
|
|
node.updateArea(ctx() as unknown as CanvasRenderingContext2D)
|
|
}
|
|
await use(reroutesComplexGraph)
|
|
},
|
|
setConnectingLinks: async (
|
|
// eslint-disable-next-line no-empty-pattern
|
|
{},
|
|
use: (mock: ReturnType<typeof vi.fn>) => Promise<void>
|
|
) => {
|
|
const mock = vi.fn()
|
|
await use(mock)
|
|
},
|
|
connector: async ({ setConnectingLinks }, use) => {
|
|
const connector = new LinkConnector(setConnectingLinks)
|
|
await use(connector)
|
|
},
|
|
createTestNode: async ({ graph }, use) => {
|
|
await use((id): LGraphNode => {
|
|
const node = new LGraphNode('test')
|
|
node.id = id
|
|
graph.add(node)
|
|
return node
|
|
})
|
|
},
|
|
|
|
validateIntegrityNoChanges: async (
|
|
{ graph, reroutesBeforeTest, expect },
|
|
use
|
|
) => {
|
|
await use(() => {
|
|
expect(graph.floatingLinks.size).toBe(1)
|
|
expect([...graph.reroutes]).toEqual(reroutesBeforeTest)
|
|
|
|
// Only the original reroute should be floating
|
|
const reroutesExceptOne = [...graph.reroutes.values()].filter(
|
|
(reroute) => reroute.id !== 1
|
|
)
|
|
for (const reroute of reroutesExceptOne) {
|
|
expect(reroute.floating).toBeUndefined()
|
|
}
|
|
})
|
|
},
|
|
|
|
validateIntegrityFloatingRemoved: async (
|
|
{ graph, reroutesBeforeTest, expect },
|
|
use
|
|
) => {
|
|
await use(() => {
|
|
expect(graph.floatingLinks.size).toBe(0)
|
|
expect([...graph.reroutes]).toEqual(reroutesBeforeTest)
|
|
|
|
for (const reroute of graph.reroutes.values()) {
|
|
expect(reroute.floating).toBeUndefined()
|
|
}
|
|
})
|
|
},
|
|
|
|
validateLinkIntegrity: async ({ graph, expect }, use) => {
|
|
await use(() => {
|
|
for (const reroute of graph.reroutes.values()) {
|
|
if (reroute.origin_id === undefined) {
|
|
expect(reroute.linkIds.size).toBe(0)
|
|
expect(reroute.floatingLinkIds.size).toBeGreaterThan(0)
|
|
}
|
|
|
|
for (const linkId of reroute.linkIds) {
|
|
const link = graph.links.get(linkId)
|
|
expect(link).toBeDefined()
|
|
expect(link!.origin_id).toEqual(reroute.origin_id)
|
|
expect(link!.origin_slot).toEqual(reroute.origin_slot)
|
|
}
|
|
for (const linkId of reroute.floatingLinkIds) {
|
|
const link = graph.floatingLinks.get(linkId)
|
|
expect(link).toBeDefined()
|
|
|
|
if (link!.target_id === -1) {
|
|
expect(link!.origin_id).not.toBe(-1)
|
|
expect(link!.origin_slot).not.toBe(-1)
|
|
expect(link!.target_slot).toBe(-1)
|
|
} else {
|
|
expect(link!.origin_id).toBe(-1)
|
|
expect(link!.origin_slot).toBe(-1)
|
|
expect(link!.target_slot).not.toBe(-1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check that all link references are valid (Can be found in the graph)
|
|
for (const node of graph.nodes.values()) {
|
|
for (const input of node.inputs) {
|
|
if (input.link) {
|
|
expect(graph.links.keys()).toContain(input.link)
|
|
expect(graph.links.get(input.link)?.target_id).toBe(node.id)
|
|
}
|
|
}
|
|
for (const output of node.outputs) {
|
|
for (const linkId of output.links ?? []) {
|
|
expect(graph.links.keys()).toContain(linkId)
|
|
expect(graph.links.get(linkId)?.origin_id).toBe(node.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const link of graph._links.values()) {
|
|
expect(
|
|
graph.getNodeById(link!.origin_id)?.outputs[link!.origin_slot].links
|
|
).toContain(link.id)
|
|
expect(
|
|
graph.getNodeById(link!.target_id)?.inputs[link!.target_slot].link
|
|
).toBe(link.id)
|
|
}
|
|
|
|
for (const link of graph.floatingLinks.values()) {
|
|
if (link.target_id === -1) {
|
|
expect(link.origin_id).not.toBe(-1)
|
|
expect(link.origin_slot).not.toBe(-1)
|
|
expect(link.target_slot).toBe(-1)
|
|
const outputFloatingLinks = graph.getNodeById(link.origin_id)
|
|
?.outputs[link.origin_slot]._floatingLinks
|
|
expect(outputFloatingLinks).toBeDefined()
|
|
expect(outputFloatingLinks).toContain(link)
|
|
} else {
|
|
expect(link.origin_id).toBe(-1)
|
|
expect(link.origin_slot).toBe(-1)
|
|
expect(link.target_slot).not.toBe(-1)
|
|
const inputFloatingLinks = graph.getNodeById(link.target_id)?.inputs[
|
|
link.target_slot
|
|
]._floatingLinks
|
|
expect(inputFloatingLinks).toBeDefined()
|
|
expect(inputFloatingLinks).toContain(link)
|
|
}
|
|
}
|
|
})
|
|
},
|
|
|
|
getNextLinkIds: async ({ graph }, use) => {
|
|
await use((linkIds, expectedExtraLinks = 0) => {
|
|
const indexes = [...new Array(linkIds.size + expectedExtraLinks).keys()]
|
|
return indexes.map((index) => graph.last_link_id + index + 1)
|
|
})
|
|
},
|
|
|
|
floatingReroute: async ({ graph, expect }, use) => {
|
|
const floatingReroute = graph.reroutes.get(1)!
|
|
expect(floatingReroute.floating).toEqual({ slotType: 'output' })
|
|
await use(floatingReroute)
|
|
}
|
|
})
|
|
|
|
function mockedNodeTitleDropEvent(node: LGraphNode): CanvasPointerEvent {
|
|
return {
|
|
canvasX: node.pos[0] + node.size[0] / 2,
|
|
canvasY: node.pos[1] + 16
|
|
} as any
|
|
}
|
|
|
|
function mockedInputDropEvent(
|
|
node: LGraphNode,
|
|
slot: number
|
|
): CanvasPointerEvent {
|
|
const pos = node.getInputPos(slot)
|
|
return {
|
|
canvasX: pos[0],
|
|
canvasY: pos[1]
|
|
} as any
|
|
}
|
|
|
|
function mockedOutputDropEvent(
|
|
node: LGraphNode,
|
|
slot: number
|
|
): CanvasPointerEvent {
|
|
const pos = node.getOutputPos(slot)
|
|
return {
|
|
canvasX: pos[0],
|
|
canvasY: pos[1]
|
|
} as any
|
|
}
|
|
|
|
describe('LinkConnector Integration', () => {
|
|
afterEach<TestContext>(({ validateLinkIntegrity }) => {
|
|
validateLinkIntegrity()
|
|
})
|
|
|
|
describe('Moving input links', () => {
|
|
test('Should move input links', ({ graph, connector }) => {
|
|
const nextLinkId = graph.last_link_id + 1
|
|
|
|
const hasInputNode = graph.getNodeById(2)!
|
|
const disconnectedNode = graph.getNodeById(9)!
|
|
|
|
const reroutesBefore = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(hasInputNode.inputs[0].link!)!
|
|
)
|
|
|
|
connector.moveInputLink(graph, hasInputNode.inputs[0])
|
|
expect(connector.state.connectingTo).toBe('input')
|
|
expect(connector.state.draggingExistingLinks).toBe(true)
|
|
expect(connector.renderLinks.length).toBe(1)
|
|
expect(connector.inputLinks.length).toBe(1)
|
|
|
|
const canvasX = disconnectedNode.pos[0] + disconnectedNode.size[0] / 2
|
|
const canvasY = disconnectedNode.pos[1] + 16
|
|
const dropEvent = { canvasX, canvasY } as any
|
|
|
|
// Drop links, ensure reset has not been run
|
|
connector.dropLinks(graph, dropEvent)
|
|
expect(connector.renderLinks.length).toBe(1)
|
|
|
|
// Test reset
|
|
connector.reset()
|
|
expect(connector.renderLinks.length).toBe(0)
|
|
expect(connector.inputLinks.length).toBe(0)
|
|
|
|
expect(disconnectedNode.inputs[0].link).toBe(nextLinkId)
|
|
expect(hasInputNode.inputs[0].link).toBeNull()
|
|
|
|
const reroutesAfter = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(disconnectedNode.inputs[0].link!)!
|
|
)
|
|
expect(reroutesAfter).toEqual(reroutesBefore)
|
|
})
|
|
|
|
test('Should connect from floating reroutes', ({
|
|
graph,
|
|
connector,
|
|
reroutesBeforeTest
|
|
}) => {
|
|
const nextLinkId = graph.last_link_id + 1
|
|
|
|
const floatingLink = graph.floatingLinks.values().next().value!
|
|
expect(floatingLink).toBeInstanceOf(LLink)
|
|
const floatingReroute = graph.reroutes.get(floatingLink.parentId!)!
|
|
|
|
const disconnectedNode = graph.getNodeById(9)!
|
|
connector.dragFromReroute(graph, floatingReroute)
|
|
|
|
expect(connector.state.connectingTo).toBe('input')
|
|
expect(connector.state.draggingExistingLinks).toBe(false)
|
|
expect(connector.renderLinks.length).toBe(1)
|
|
expect(connector.inputLinks.length).toBe(0)
|
|
|
|
const canvasX = disconnectedNode.pos[0] + disconnectedNode.size[0] / 2
|
|
const canvasY = disconnectedNode.pos[1] + 16
|
|
const dropEvent = { canvasX, canvasY } as any
|
|
|
|
connector.dropLinks(graph, dropEvent)
|
|
connector.reset()
|
|
expect(connector.renderLinks.length).toBe(0)
|
|
expect(connector.inputLinks.length).toBe(0)
|
|
|
|
// New link should have been created
|
|
expect(disconnectedNode.inputs[0].link).toBe(nextLinkId)
|
|
|
|
// Check graph integrity
|
|
expect(graph.floatingLinks.size).toBe(0)
|
|
expect([...graph.reroutes]).toEqual(reroutesBeforeTest)
|
|
|
|
// All reroute floating property should be cleared
|
|
for (const reroute of graph.reroutes.values()) {
|
|
expect(reroute.floating).toBeUndefined()
|
|
}
|
|
})
|
|
|
|
test('Should drop floating links when both sides are disconnected', ({
|
|
graph,
|
|
reroutesBeforeTest
|
|
}) => {
|
|
expect(graph.floatingLinks.size).toBe(1)
|
|
|
|
const floatingOutNode = graph.getNodeById(1)!
|
|
floatingOutNode.disconnectOutput(0)
|
|
|
|
// Should have lost one reroute
|
|
expect(graph.reroutes.size).toBe(reroutesBeforeTest.length - 1)
|
|
expect(graph.reroutes.get(1)).toBeUndefined()
|
|
|
|
// The two normal links should now be floating
|
|
expect(graph.floatingLinks.size).toBe(2)
|
|
|
|
graph.getNodeById(2)!.disconnectInput(0, true)
|
|
expect(graph.floatingLinks.size).toBe(1)
|
|
|
|
graph.getNodeById(3)!.disconnectInput(0, false)
|
|
expect(graph.floatingLinks.size).toBe(0)
|
|
|
|
// Removed 4 reroutes
|
|
expect(graph.reroutes.size).toBe(9)
|
|
|
|
// All four nodes should have no links
|
|
for (const nodeId of [1, 2, 3, 9]) {
|
|
const {
|
|
inputs: [input],
|
|
outputs: [output]
|
|
} = graph.getNodeById(nodeId)!
|
|
|
|
expect(input.link).toBeNull()
|
|
expect(output.links?.length).toBeOneOf([0, undefined])
|
|
|
|
expect(input._floatingLinks?.size).toBeOneOf([0, undefined])
|
|
expect(output._floatingLinks?.size).toBeOneOf([0, undefined])
|
|
}
|
|
})
|
|
|
|
test('Should prevent node loopback when dropping on node', ({
|
|
graph,
|
|
connector
|
|
}) => {
|
|
const hasOutputNode = graph.getNodeById(1)!
|
|
const hasInputNode = graph.getNodeById(2)!
|
|
const hasInputNode2 = graph.getNodeById(3)!
|
|
|
|
const reroutesBefore = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(hasInputNode.inputs[0].link!)!
|
|
)
|
|
|
|
const atOutputNodeEvent = mockedNodeTitleDropEvent(hasOutputNode)
|
|
|
|
connector.moveInputLink(graph, hasInputNode.inputs[0])
|
|
connector.dropLinks(graph, atOutputNodeEvent)
|
|
connector.reset()
|
|
|
|
const outputNodes = hasOutputNode.getOutputNodes(0)
|
|
expect(outputNodes).toEqual([hasInputNode, hasInputNode2])
|
|
|
|
const reroutesAfter = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(hasInputNode.inputs[0].link!)!
|
|
)
|
|
expect(reroutesAfter).toEqual(reroutesBefore)
|
|
})
|
|
|
|
test('Should prevent node loopback when dropping on input', ({
|
|
graph,
|
|
connector
|
|
}) => {
|
|
const hasOutputNode = graph.getNodeById(1)!
|
|
const hasInputNode = graph.getNodeById(2)!
|
|
|
|
const originalOutputNodes = hasOutputNode.getOutputNodes(0)
|
|
const reroutesBefore = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(hasInputNode.inputs[0].link!)!
|
|
)
|
|
|
|
const atHasOutputNode = mockedInputDropEvent(hasOutputNode, 0)
|
|
|
|
connector.moveInputLink(graph, hasInputNode.inputs[0])
|
|
connector.dropLinks(graph, atHasOutputNode)
|
|
connector.reset()
|
|
|
|
const outputNodes = hasOutputNode.getOutputNodes(0)
|
|
expect(outputNodes).toEqual(originalOutputNodes)
|
|
|
|
const reroutesAfter = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(hasInputNode.inputs[0].link!)!
|
|
)
|
|
expect(reroutesAfter).toEqual(reroutesBefore)
|
|
})
|
|
})
|
|
|
|
describe('Moving output links', () => {
|
|
test('Should move output links', ({ graph, connector }) => {
|
|
const nextLinkIds = [graph.last_link_id + 1, graph.last_link_id + 2]
|
|
|
|
const hasOutputNode = graph.getNodeById(1)!
|
|
const disconnectedNode = graph.getNodeById(9)!
|
|
|
|
const reroutesBefore = hasOutputNode.outputs[0].links
|
|
?.map((linkId) => graph.links.get(linkId)!)
|
|
.map((link) => LLink.getReroutes(graph, link))
|
|
|
|
connector.moveOutputLink(graph, hasOutputNode.outputs[0])
|
|
expect(connector.state.connectingTo).toBe('output')
|
|
expect(connector.state.draggingExistingLinks).toBe(true)
|
|
expect(connector.renderLinks.length).toBe(3)
|
|
expect(connector.outputLinks.length).toBe(2)
|
|
expect(connector.floatingLinks.length).toBe(1)
|
|
|
|
const canvasX = disconnectedNode.pos[0] + disconnectedNode.size[0] / 2
|
|
const canvasY = disconnectedNode.pos[1] + 16
|
|
const dropEvent = { canvasX, canvasY } as any
|
|
|
|
connector.dropLinks(graph, dropEvent)
|
|
connector.reset()
|
|
expect(connector.renderLinks.length).toBe(0)
|
|
expect(connector.outputLinks.length).toBe(0)
|
|
|
|
expect(disconnectedNode.outputs[0].links).toEqual(nextLinkIds)
|
|
expect(hasOutputNode.outputs[0].links).toEqual([])
|
|
|
|
const reroutesAfter = disconnectedNode.outputs[0].links
|
|
?.map((linkId) => graph.links.get(linkId)!)
|
|
.map((link) => LLink.getReroutes(graph, link))
|
|
|
|
expect(reroutesAfter).toEqual(reroutesBefore)
|
|
})
|
|
|
|
test('Should connect to floating reroutes from outputs', ({
|
|
graph,
|
|
connector,
|
|
reroutesBeforeTest
|
|
}) => {
|
|
const nextLinkIds = [graph.last_link_id + 1, graph.last_link_id + 2]
|
|
|
|
const floatingOutNode = graph.getNodeById(1)!
|
|
floatingOutNode.disconnectOutput(0)
|
|
|
|
// Should have lost one reroute
|
|
expect(graph.reroutes.size).toBe(reroutesBeforeTest.length - 1)
|
|
expect(graph.reroutes.get(1)).toBeUndefined()
|
|
|
|
// The two normal links should now be floating
|
|
expect(graph.floatingLinks.size).toBe(2)
|
|
|
|
const disconnectedNode = graph.getNodeById(9)!
|
|
connector.dragNewFromOutput(
|
|
graph,
|
|
disconnectedNode,
|
|
disconnectedNode.outputs[0]
|
|
)
|
|
|
|
expect(connector.state.connectingTo).toBe('input')
|
|
expect(connector.state.draggingExistingLinks).toBe(false)
|
|
expect(connector.renderLinks.length).toBe(1)
|
|
expect(connector.outputLinks.length).toBe(0)
|
|
expect(connector.floatingLinks.length).toBe(0)
|
|
|
|
const floatingLink = graph.floatingLinks.values().next().value!
|
|
expect(floatingLink).toBeInstanceOf(LLink)
|
|
const floatingReroute = LLink.getReroutes(graph, floatingLink)[0]
|
|
|
|
const canvasX = floatingReroute.pos[0]
|
|
const canvasY = floatingReroute.pos[1]
|
|
const dropEvent = { canvasX, canvasY } as any
|
|
|
|
connector.dropLinks(graph, dropEvent)
|
|
connector.reset()
|
|
expect(connector.renderLinks.length).toBe(0)
|
|
expect(connector.outputLinks.length).toBe(0)
|
|
|
|
// New link should have been created
|
|
expect(disconnectedNode.outputs[0].links).toEqual(nextLinkIds)
|
|
|
|
// Check graph integrity
|
|
expect(graph.floatingLinks.size).toBe(0)
|
|
expect([...graph.reroutes]).toEqual(reroutesBeforeTest.slice(1))
|
|
|
|
for (const reroute of graph.reroutes.values()) {
|
|
expect(reroute.floating).toBeUndefined()
|
|
}
|
|
})
|
|
|
|
test('Should drop floating links when both sides are disconnected', ({
|
|
graph,
|
|
reroutesBeforeTest
|
|
}) => {
|
|
expect(graph.floatingLinks.size).toBe(1)
|
|
|
|
graph.getNodeById(2)!.disconnectInput(0, true)
|
|
expect(graph.floatingLinks.size).toBe(1)
|
|
|
|
// Only the original reroute should be floating
|
|
const reroutesExceptOne = [...graph.reroutes.values()].filter(
|
|
(reroute) => reroute.id !== 1
|
|
)
|
|
for (const reroute of reroutesExceptOne) {
|
|
expect(reroute.floating).toBeUndefined()
|
|
}
|
|
|
|
graph.getNodeById(3)!.disconnectInput(0, true)
|
|
expect([...graph.reroutes]).toEqual(reroutesBeforeTest)
|
|
|
|
// The normal link should now be floating
|
|
expect(graph.floatingLinks.size).toBe(2)
|
|
expect(graph.reroutes.get(3)!.floating).toEqual({ slotType: 'output' })
|
|
|
|
const floatingOutNode = graph.getNodeById(1)!
|
|
floatingOutNode.disconnectOutput(0)
|
|
|
|
// Should have lost one reroute
|
|
expect(graph.reroutes.size).toBe(9)
|
|
expect(graph.reroutes.get(1)).toBeUndefined()
|
|
|
|
// Removed 4 reroutes
|
|
expect(graph.reroutes.size).toBe(9)
|
|
|
|
// All four nodes should have no links
|
|
for (const nodeId of [1, 2, 3, 9]) {
|
|
const {
|
|
inputs: [input],
|
|
outputs: [output]
|
|
} = graph.getNodeById(nodeId)!
|
|
|
|
expect(input.link).toBeNull()
|
|
expect(output.links?.length).toBeOneOf([0, undefined])
|
|
|
|
expect(input._floatingLinks?.size).toBeOneOf([0, undefined])
|
|
expect(output._floatingLinks?.size).toBeOneOf([0, undefined])
|
|
}
|
|
})
|
|
|
|
test('Should support moving multiple output links to a floating reroute', ({
|
|
graph,
|
|
connector,
|
|
floatingReroute,
|
|
validateIntegrityFloatingRemoved
|
|
}) => {
|
|
const manyOutputsNode = graph.getNodeById(4)!
|
|
const canvasX = floatingReroute.pos[0]
|
|
const canvasY = floatingReroute.pos[1]
|
|
const floatingRerouteEvent = { canvasX, canvasY } as any
|
|
|
|
connector.moveOutputLink(graph, manyOutputsNode.outputs[0])
|
|
connector.dropLinks(graph, floatingRerouteEvent)
|
|
connector.reset()
|
|
|
|
expect(manyOutputsNode.outputs[0].links).toEqual([])
|
|
expect(floatingReroute.linkIds.size).toBe(4)
|
|
|
|
validateIntegrityFloatingRemoved()
|
|
})
|
|
|
|
test('Should prevent dragging from an output to a child reroute', ({
|
|
graph,
|
|
connector,
|
|
floatingReroute
|
|
}) => {
|
|
const manyOutputsNode = graph.getNodeById(4)!
|
|
|
|
const reroute7 = graph.reroutes.get(7)!
|
|
const reroute10 = graph.reroutes.get(10)!
|
|
const reroute13 = graph.reroutes.get(13)!
|
|
|
|
const canvasX = reroute7.pos[0]
|
|
const canvasY = reroute7.pos[1]
|
|
const reroute7Event = { canvasX, canvasY } as any
|
|
|
|
const toSortedRerouteChain = (linkIds: number[]) =>
|
|
linkIds
|
|
.map((x) => graph.links.get(x)!)
|
|
.map((x) => LLink.getReroutes(graph, x))
|
|
.sort((a, b) => a.at(-1)!.id - b.at(-1)!.id)
|
|
|
|
const reroutesBefore = toSortedRerouteChain(
|
|
manyOutputsNode.outputs[0].links!
|
|
)
|
|
|
|
connector.moveOutputLink(graph, manyOutputsNode.outputs[0])
|
|
expect(connector.isRerouteValidDrop(reroute7)).toBe(false)
|
|
expect(connector.isRerouteValidDrop(reroute10)).toBe(false)
|
|
expect(connector.isRerouteValidDrop(reroute13)).toBe(false)
|
|
|
|
// Prevent link disconnect when dropped on canvas (just for this test)
|
|
connector.events.addEventListener(
|
|
'dropped-on-canvas',
|
|
(e) => e.preventDefault(),
|
|
{ once: true }
|
|
)
|
|
connector.dropLinks(graph, reroute7Event)
|
|
connector.reset()
|
|
|
|
const reroutesAfter = toSortedRerouteChain(
|
|
manyOutputsNode.outputs[0].links!
|
|
)
|
|
expect(reroutesAfter).toEqual(reroutesBefore)
|
|
|
|
expect(graph.floatingLinks.size).toBe(1)
|
|
expect(floatingReroute.linkIds.size).toBe(0)
|
|
})
|
|
|
|
test('Should prevent node loopback when dropping on node', ({
|
|
graph,
|
|
connector
|
|
}) => {
|
|
const hasOutputNode = graph.getNodeById(1)!
|
|
const hasInputNode = graph.getNodeById(2)!
|
|
|
|
const reroutesBefore = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(hasOutputNode.outputs[0].links![0])!
|
|
)
|
|
|
|
const atInputNodeEvent = mockedNodeTitleDropEvent(hasInputNode)
|
|
|
|
connector.moveOutputLink(graph, hasOutputNode.outputs[0])
|
|
connector.dropLinks(graph, atInputNodeEvent)
|
|
connector.reset()
|
|
|
|
expect(hasOutputNode.getOutputNodes(0)).toEqual([hasInputNode])
|
|
expect(hasInputNode.getOutputNodes(0)).toEqual([graph.getNodeById(3)])
|
|
|
|
// Moved link should have the same reroutes
|
|
const reroutesAfter = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(hasInputNode.outputs[0].links![0])!
|
|
)
|
|
expect(reroutesAfter).toEqual(reroutesBefore)
|
|
|
|
// Link recreated to avoid loopback should have no reroutes
|
|
const reroutesAfter2 = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(hasOutputNode.outputs[0].links![0])!
|
|
)
|
|
expect(reroutesAfter2).toEqual([])
|
|
})
|
|
|
|
test('Should prevent node loopback when dropping on output', ({
|
|
graph,
|
|
connector
|
|
}) => {
|
|
const hasOutputNode = graph.getNodeById(1)!
|
|
const hasInputNode = graph.getNodeById(2)!
|
|
|
|
const reroutesBefore = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(hasOutputNode.outputs[0].links![0])!
|
|
)
|
|
|
|
const atInputNodeOutSlot = mockedOutputDropEvent(hasInputNode, 0)
|
|
|
|
connector.moveOutputLink(graph, hasOutputNode.outputs[0])
|
|
connector.dropLinks(graph, atInputNodeOutSlot)
|
|
connector.reset()
|
|
|
|
expect(hasOutputNode.getOutputNodes(0)).toEqual([hasInputNode])
|
|
expect(hasInputNode.getOutputNodes(0)).toEqual([graph.getNodeById(3)])
|
|
|
|
// Moved link should have the same reroutes
|
|
const reroutesAfter = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(hasInputNode.outputs[0].links![0])!
|
|
)
|
|
expect(reroutesAfter).toEqual(reroutesBefore)
|
|
|
|
// Link recreated to avoid loopback should have no reroutes
|
|
const reroutesAfter2 = LLink.getReroutes(
|
|
graph,
|
|
graph.links.get(hasOutputNode.outputs[0].links![0])!
|
|
)
|
|
expect(reroutesAfter2).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('Floating links', () => {
|
|
test('Removed when connecting from reroute to input', ({
|
|
graph,
|
|
connector,
|
|
floatingReroute
|
|
}) => {
|
|
const disconnectedNode = graph.getNodeById(9)!
|
|
const canvasX = disconnectedNode.pos[0]
|
|
const canvasY = disconnectedNode.pos[1]
|
|
|
|
connector.dragFromReroute(graph, floatingReroute)
|
|
connector.dropLinks(graph, { canvasX, canvasY } as any)
|
|
connector.reset()
|
|
|
|
expect(graph.floatingLinks.size).toBe(0)
|
|
expect(floatingReroute.floating).toBeUndefined()
|
|
})
|
|
|
|
test('Removed when connecting from reroute to another reroute', ({
|
|
graph,
|
|
connector,
|
|
floatingReroute,
|
|
validateIntegrityFloatingRemoved
|
|
}) => {
|
|
const reroute8 = graph.reroutes.get(8)!
|
|
const canvasX = reroute8.pos[0]
|
|
const canvasY = reroute8.pos[1]
|
|
|
|
connector.dragFromReroute(graph, floatingReroute)
|
|
connector.dropLinks(graph, { canvasX, canvasY } as any)
|
|
connector.reset()
|
|
|
|
expect(graph.floatingLinks.size).toBe(0)
|
|
expect(floatingReroute.floating).toBeUndefined()
|
|
expect(reroute8.floating).toBeUndefined()
|
|
|
|
validateIntegrityFloatingRemoved()
|
|
})
|
|
|
|
test('Dropping a floating input link onto input slot disconnects the existing link', ({
|
|
graph,
|
|
connector
|
|
}) => {
|
|
const manyOutputsNode = graph.getNodeById(4)!
|
|
manyOutputsNode.disconnectOutput(0)
|
|
|
|
const floatingInputNode = graph.getNodeById(6)!
|
|
const fromFloatingInput = floatingInputNode.inputs[0]
|
|
|
|
const hasInputNode = graph.getNodeById(2)!
|
|
const toInput = hasInputNode.inputs[0]
|
|
|
|
connector.moveInputLink(graph, fromFloatingInput)
|
|
const dropEvent = mockedInputDropEvent(hasInputNode, 0)
|
|
connector.dropLinks(graph, dropEvent)
|
|
connector.reset()
|
|
|
|
expect(fromFloatingInput.link).toBeNull()
|
|
expect(fromFloatingInput._floatingLinks?.size).toBe(0)
|
|
|
|
expect(toInput.link).toBeNull()
|
|
expect(toInput._floatingLinks?.size).toBe(1)
|
|
})
|
|
|
|
test('Allow reroutes to be used as manual switches', ({
|
|
graph,
|
|
connector,
|
|
floatingReroute,
|
|
validateIntegrityNoChanges
|
|
}) => {
|
|
const rerouteWithTwoLinks = graph.reroutes.get(3)!
|
|
const targetNode = graph.getNodeById(2)!
|
|
|
|
const targetDropEvent = mockedInputDropEvent(targetNode, 0)
|
|
|
|
connector.dragFromReroute(graph, floatingReroute)
|
|
connector.dropLinks(graph, targetDropEvent)
|
|
connector.reset()
|
|
|
|
// Link should have been moved to the floating reroute, and no floating links should remain
|
|
expect(rerouteWithTwoLinks.floating).toBeUndefined()
|
|
expect(floatingReroute.floating).toBeUndefined()
|
|
expect(rerouteWithTwoLinks.floatingLinkIds.size).toBe(0)
|
|
expect(floatingReroute.floatingLinkIds.size).toBe(0)
|
|
expect(rerouteWithTwoLinks.linkIds.size).toBe(1)
|
|
expect(floatingReroute.linkIds.size).toBe(1)
|
|
|
|
// Move the link again
|
|
connector.dragFromReroute(graph, rerouteWithTwoLinks)
|
|
connector.dropLinks(graph, targetDropEvent)
|
|
connector.reset()
|
|
|
|
// Everything should be back the way it was when we started
|
|
expect(rerouteWithTwoLinks.floating).toBeUndefined()
|
|
expect(floatingReroute.floating).toEqual({ slotType: 'output' })
|
|
expect(rerouteWithTwoLinks.floatingLinkIds.size).toBe(0)
|
|
expect(floatingReroute.floatingLinkIds.size).toBe(1)
|
|
expect(rerouteWithTwoLinks.linkIds.size).toBe(2)
|
|
expect(floatingReroute.linkIds.size).toBe(0)
|
|
|
|
validateIntegrityNoChanges()
|
|
})
|
|
})
|
|
|
|
test('Should drop floating links when both sides are disconnected', ({
|
|
graph,
|
|
connector,
|
|
reroutesBeforeTest,
|
|
validateIntegrityNoChanges
|
|
}) => {
|
|
const floatingOutNode = graph.getNodeById(1)!
|
|
connector.moveOutputLink(graph, floatingOutNode.outputs[0])
|
|
|
|
const manyOutputsNode = graph.getNodeById(4)!
|
|
const dropEvent = {
|
|
canvasX: manyOutputsNode.pos[0],
|
|
canvasY: manyOutputsNode.pos[1]
|
|
} as any
|
|
connector.dropLinks(graph, dropEvent)
|
|
connector.reset()
|
|
|
|
const output = manyOutputsNode.outputs[0]
|
|
expect(output.links!.length).toBe(6)
|
|
expect(output._floatingLinks!.size).toBe(1)
|
|
|
|
validateIntegrityNoChanges()
|
|
|
|
// Move again
|
|
connector.moveOutputLink(graph, manyOutputsNode.outputs[0])
|
|
|
|
const disconnectedNode = graph.getNodeById(9)!
|
|
dropEvent.canvasX = disconnectedNode.pos[0]
|
|
dropEvent.canvasY = disconnectedNode.pos[1]
|
|
connector.dropLinks(graph, dropEvent)
|
|
connector.reset()
|
|
|
|
const newOutput = disconnectedNode.outputs[0]
|
|
expect(newOutput.links!.length).toBe(6)
|
|
expect(newOutput._floatingLinks!.size).toBe(1)
|
|
|
|
validateIntegrityNoChanges()
|
|
|
|
disconnectedNode.disconnectOutput(0)
|
|
|
|
expect(newOutput._floatingLinks!.size).toBe(0)
|
|
expect(graph.floatingLinks.size).toBe(6)
|
|
|
|
// The final reroutes should all be floating
|
|
for (const reroute of graph.reroutes.values()) {
|
|
if ([3, 7, 15, 12].includes(reroute.id)) {
|
|
expect(reroute.floating).toEqual({ slotType: 'input' })
|
|
} else {
|
|
expect(reroute.floating).toBeUndefined()
|
|
}
|
|
}
|
|
|
|
// Removed one reroute
|
|
expect(graph.reroutes.size).toBe(reroutesBeforeTest.length - 1)
|
|
|
|
// Original nodes should have no links
|
|
for (const nodeId of [1, 4]) {
|
|
const {
|
|
inputs: [input],
|
|
outputs: [output]
|
|
} = graph.getNodeById(nodeId)!
|
|
|
|
expect(input.link).toBeNull()
|
|
expect(output.links?.length).toBeOneOf([0, undefined])
|
|
|
|
expect(input._floatingLinks?.size).toBeOneOf([0, undefined])
|
|
expect(output._floatingLinks?.size).toBeOneOf([0, undefined])
|
|
}
|
|
})
|
|
|
|
type TestData = {
|
|
/** Drop link on this reroute */
|
|
targetRerouteId: number
|
|
/** Parent reroutes of the target reroute */
|
|
parentIds: number[]
|
|
/** Number of links before the drop */
|
|
linksBefore: number[]
|
|
/** Number of links after the drop */
|
|
linksAfter: (number | undefined)[]
|
|
/** Whether to run the integrity check */
|
|
runIntegrityCheck: boolean
|
|
}
|
|
|
|
test.for<TestData>([
|
|
{
|
|
targetRerouteId: 8,
|
|
parentIds: [13, 10],
|
|
linksBefore: [3, 4],
|
|
linksAfter: [1, 2],
|
|
runIntegrityCheck: true
|
|
},
|
|
{
|
|
targetRerouteId: 7,
|
|
parentIds: [6, 8, 13, 10],
|
|
linksBefore: [2, 2, 3, 4],
|
|
linksAfter: [undefined, undefined, 1, 2],
|
|
runIntegrityCheck: false
|
|
},
|
|
{
|
|
targetRerouteId: 6,
|
|
parentIds: [8, 13, 10],
|
|
linksBefore: [2, 3, 4],
|
|
linksAfter: [undefined, 1, 2],
|
|
runIntegrityCheck: false
|
|
},
|
|
{
|
|
targetRerouteId: 13,
|
|
parentIds: [10],
|
|
linksBefore: [4],
|
|
linksAfter: [1],
|
|
runIntegrityCheck: true
|
|
},
|
|
{
|
|
targetRerouteId: 4,
|
|
parentIds: [],
|
|
linksBefore: [],
|
|
linksAfter: [],
|
|
runIntegrityCheck: true
|
|
},
|
|
{
|
|
targetRerouteId: 2,
|
|
parentIds: [4],
|
|
linksBefore: [2],
|
|
linksAfter: [undefined],
|
|
runIntegrityCheck: false
|
|
},
|
|
{
|
|
targetRerouteId: 3,
|
|
parentIds: [2, 4],
|
|
linksBefore: [2, 2],
|
|
linksAfter: [0, 0],
|
|
runIntegrityCheck: true
|
|
}
|
|
])(
|
|
'Should allow reconnect from output to any reroute',
|
|
(
|
|
{
|
|
targetRerouteId,
|
|
parentIds,
|
|
linksBefore,
|
|
linksAfter,
|
|
runIntegrityCheck
|
|
},
|
|
{ graph, connector, validateIntegrityNoChanges, getNextLinkIds }
|
|
) => {
|
|
const linkCreatedCallback = vi.fn()
|
|
connector.listenUntilReset('link-created', linkCreatedCallback)
|
|
|
|
const disconnectedNode = graph.getNodeById(9)!
|
|
|
|
// Parent reroutes of the target reroute
|
|
for (const [index, parentId] of parentIds.entries()) {
|
|
const reroute = graph.reroutes.get(parentId)!
|
|
expect(reroute.linkIds.size).toBe(linksBefore[index])
|
|
}
|
|
|
|
const targetReroute = graph.reroutes.get(targetRerouteId)!
|
|
const nextLinkIds = getNextLinkIds(targetReroute.linkIds)
|
|
const dropEvent = {
|
|
canvasX: targetReroute.pos[0],
|
|
canvasY: targetReroute.pos[1]
|
|
} as any
|
|
|
|
connector.dragNewFromOutput(
|
|
graph,
|
|
disconnectedNode,
|
|
disconnectedNode.outputs[0]
|
|
)
|
|
connector.dropLinks(graph, dropEvent)
|
|
connector.reset()
|
|
|
|
expect(disconnectedNode.outputs[0].links).toEqual(nextLinkIds)
|
|
expect([...targetReroute.linkIds.values()]).toEqual(nextLinkIds)
|
|
|
|
// Parent reroutes should have lost the links or been removed
|
|
for (const [index, parentId] of parentIds.entries()) {
|
|
const reroute = graph.reroutes.get(parentId)!
|
|
if (linksAfter[index] === undefined) {
|
|
expect(reroute).not.toBeUndefined()
|
|
} else {
|
|
expect(reroute.linkIds.size).toBe(linksAfter[index])
|
|
}
|
|
}
|
|
|
|
expect(linkCreatedCallback).toHaveBeenCalledTimes(nextLinkIds.length)
|
|
|
|
if (runIntegrityCheck) {
|
|
validateIntegrityNoChanges()
|
|
}
|
|
}
|
|
)
|
|
|
|
type ReconnectTestData = {
|
|
/** Drag link from this reroute */
|
|
fromRerouteId: number
|
|
/** Drop link on this reroute */
|
|
toRerouteId: number
|
|
/** Reroute IDs that should be removed from the resultant reroute chain */
|
|
shouldBeRemoved: number[]
|
|
/** Reroutes that should have NONE of the link IDs that toReroute has */
|
|
shouldHaveLinkIdsRemoved: number[]
|
|
/** Whether to test floating inputs */
|
|
testFloatingInputs?: true
|
|
/** Number of expected extra links to be created */
|
|
expectedExtraLinks?: number
|
|
}
|
|
|
|
test.for<ReconnectTestData>([
|
|
{
|
|
fromRerouteId: 10,
|
|
toRerouteId: 15,
|
|
shouldBeRemoved: [14],
|
|
shouldHaveLinkIdsRemoved: [13, 8, 6, 7]
|
|
},
|
|
{
|
|
fromRerouteId: 8,
|
|
toRerouteId: 2,
|
|
shouldBeRemoved: [4],
|
|
shouldHaveLinkIdsRemoved: []
|
|
},
|
|
{
|
|
fromRerouteId: 3,
|
|
toRerouteId: 12,
|
|
shouldBeRemoved: [11],
|
|
shouldHaveLinkIdsRemoved: [10, 13, 14, 15, 8, 6, 7]
|
|
},
|
|
{
|
|
fromRerouteId: 15,
|
|
toRerouteId: 7,
|
|
shouldBeRemoved: [8, 6],
|
|
shouldHaveLinkIdsRemoved: []
|
|
},
|
|
{
|
|
fromRerouteId: 1,
|
|
toRerouteId: 7,
|
|
shouldBeRemoved: [8, 6],
|
|
shouldHaveLinkIdsRemoved: []
|
|
},
|
|
{
|
|
fromRerouteId: 1,
|
|
toRerouteId: 10,
|
|
shouldBeRemoved: [],
|
|
shouldHaveLinkIdsRemoved: []
|
|
},
|
|
{
|
|
fromRerouteId: 4,
|
|
toRerouteId: 8,
|
|
shouldBeRemoved: [],
|
|
shouldHaveLinkIdsRemoved: [],
|
|
testFloatingInputs: true,
|
|
expectedExtraLinks: 2
|
|
},
|
|
{
|
|
fromRerouteId: 2,
|
|
toRerouteId: 12,
|
|
shouldBeRemoved: [11],
|
|
shouldHaveLinkIdsRemoved: [],
|
|
testFloatingInputs: true,
|
|
expectedExtraLinks: 1
|
|
}
|
|
])(
|
|
'Should allow connecting from reroutes to another reroute',
|
|
(
|
|
{
|
|
fromRerouteId,
|
|
toRerouteId,
|
|
shouldBeRemoved,
|
|
shouldHaveLinkIdsRemoved,
|
|
testFloatingInputs,
|
|
expectedExtraLinks
|
|
},
|
|
{ graph, connector, getNextLinkIds }
|
|
) => {
|
|
if (testFloatingInputs) {
|
|
// Start by disconnecting the output of the 3x3 array of reroutes
|
|
graph.getNodeById(4)!.disconnectOutput(0)
|
|
}
|
|
|
|
const fromReroute = graph.reroutes.get(fromRerouteId)!
|
|
const toReroute = graph.reroutes.get(toRerouteId)!
|
|
const nextLinkIds = getNextLinkIds(toReroute.linkIds, expectedExtraLinks)
|
|
|
|
const originalParentChain = LLink.getReroutes(graph, toReroute)
|
|
|
|
const sortAndJoin = (numbers: Iterable<number>) =>
|
|
[...numbers].sort().join(',')
|
|
const hasIdenticalLinks = (a: Reroute, b: Reroute) =>
|
|
sortAndJoin(a.linkIds) === sortAndJoin(b.linkIds) &&
|
|
sortAndJoin(a.floatingLinkIds) === sortAndJoin(b.floatingLinkIds)
|
|
|
|
// Sanity check shouldBeRemoved
|
|
const reroutesWithIdenticalLinkIds = originalParentChain.filter(
|
|
(parent) => hasIdenticalLinks(parent, toReroute)
|
|
)
|
|
expect(reroutesWithIdenticalLinkIds.map((reroute) => reroute.id)).toEqual(
|
|
shouldBeRemoved
|
|
)
|
|
|
|
connector.dragFromReroute(graph, fromReroute)
|
|
|
|
const dropEvent = {
|
|
canvasX: toReroute.pos[0],
|
|
canvasY: toReroute.pos[1]
|
|
} as any
|
|
connector.dropLinks(graph, dropEvent)
|
|
connector.reset()
|
|
|
|
const newParentChain = LLink.getReroutes(graph, toReroute)
|
|
for (const rerouteId of shouldBeRemoved) {
|
|
expect(originalParentChain.map((reroute) => reroute.id)).toContain(
|
|
rerouteId
|
|
)
|
|
expect(newParentChain.map((reroute) => reroute.id)).not.toContain(
|
|
rerouteId
|
|
)
|
|
}
|
|
|
|
expect([...toReroute.linkIds.values()]).toEqual(nextLinkIds)
|
|
|
|
for (const rerouteId of shouldBeRemoved) {
|
|
const reroute = graph.reroutes.get(rerouteId)!
|
|
if (testFloatingInputs) {
|
|
// Already-floating reroutes should be removed
|
|
expect(reroute).toBeUndefined()
|
|
} else {
|
|
// Non-floating reroutes should still exist
|
|
expect(reroute).not.toBeUndefined()
|
|
}
|
|
}
|
|
|
|
for (const rerouteId of shouldHaveLinkIdsRemoved) {
|
|
const reroute = graph.reroutes.get(rerouteId)!
|
|
for (const linkId of toReroute.linkIds) {
|
|
expect(reroute.linkIds).not.toContain(linkId)
|
|
}
|
|
}
|
|
|
|
// Validate all links in a reroute share the same origin
|
|
for (const reroute of graph.reroutes.values()) {
|
|
for (const linkId of reroute.linkIds) {
|
|
const link = graph.links.get(linkId)
|
|
expect(link?.origin_id).toEqual(reroute.origin_id)
|
|
expect(link?.origin_slot).toEqual(reroute.origin_slot)
|
|
}
|
|
for (const linkId of reroute.floatingLinkIds) {
|
|
if (reroute.origin_id === undefined) continue
|
|
|
|
const link = graph.floatingLinks.get(linkId)
|
|
expect(link?.origin_id).toEqual(reroute.origin_id)
|
|
expect(link?.origin_slot).toEqual(reroute.origin_slot)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
test.for([
|
|
{ from: 8, to: 13 },
|
|
{ from: 7, to: 13 },
|
|
{ from: 6, to: 13 },
|
|
{ from: 13, to: 10 },
|
|
{ from: 14, to: 10 },
|
|
{ from: 15, to: 10 },
|
|
{ from: 14, to: 13 },
|
|
{ from: 10, to: 10 }
|
|
])(
|
|
'Connecting reroutes to invalid targets should do nothing',
|
|
({ from, to }, { graph, connector, validateIntegrityNoChanges }) => {
|
|
const listener = vi.fn()
|
|
connector.listenUntilReset('link-created', listener)
|
|
|
|
const fromReroute = graph.reroutes.get(from)!
|
|
const toReroute = graph.reroutes.get(to)!
|
|
|
|
const dropEvent = {
|
|
canvasX: toReroute.pos[0],
|
|
canvasY: toReroute.pos[1]
|
|
} as any
|
|
|
|
connector.dragFromReroute(graph, fromReroute)
|
|
connector.dropLinks(graph, dropEvent)
|
|
connector.reset()
|
|
|
|
expect(listener).not.toHaveBeenCalled()
|
|
validateIntegrityNoChanges()
|
|
}
|
|
)
|
|
|
|
const nodeReroutePairs = [
|
|
{ nodeId: 1, rerouteId: 1 },
|
|
{ nodeId: 1, rerouteId: 3 },
|
|
{ nodeId: 1, rerouteId: 4 },
|
|
{ nodeId: 1, rerouteId: 2 },
|
|
{ nodeId: 4, rerouteId: 7 },
|
|
{ nodeId: 4, rerouteId: 6 },
|
|
{ nodeId: 4, rerouteId: 8 },
|
|
{ nodeId: 4, rerouteId: 10 },
|
|
{ nodeId: 4, rerouteId: 12 }
|
|
]
|
|
test.for(nodeReroutePairs)(
|
|
'Should ignore connections from input to same node via reroutes',
|
|
(
|
|
{ nodeId, rerouteId },
|
|
{ graph, connector, validateIntegrityNoChanges }
|
|
) => {
|
|
const listener = vi.fn()
|
|
connector.listenUntilReset('link-created', listener)
|
|
|
|
const node = graph.getNodeById(nodeId)!
|
|
const input = node.inputs[0]
|
|
const reroute = graph.getReroute(rerouteId)!
|
|
const dropEvent = {
|
|
canvasX: reroute.pos[0],
|
|
canvasY: reroute.pos[1]
|
|
} as any
|
|
|
|
connector.dragNewFromInput(graph, node, input)
|
|
connector.dropLinks(graph, dropEvent)
|
|
connector.reset()
|
|
|
|
expect(listener).not.toHaveBeenCalled()
|
|
validateIntegrityNoChanges()
|
|
|
|
// No links should have the same origin_id and target_id
|
|
for (const link of graph.links.values()) {
|
|
expect(link.origin_id).not.toEqual(link.target_id)
|
|
}
|
|
}
|
|
)
|
|
|
|
test.for(nodeReroutePairs)(
|
|
'Should ignore connections looping back to the origin node from a reroute',
|
|
(
|
|
{ nodeId, rerouteId },
|
|
{ graph, connector, validateIntegrityNoChanges }
|
|
) => {
|
|
const listener = vi.fn()
|
|
connector.listenUntilReset('link-created', listener)
|
|
|
|
const node = graph.getNodeById(nodeId)!
|
|
const reroute = graph.getReroute(rerouteId)!
|
|
const dropEvent = { canvasX: node.pos[0], canvasY: node.pos[1] } as any
|
|
|
|
connector.dragFromReroute(graph, reroute)
|
|
connector.dropLinks(graph, dropEvent)
|
|
connector.reset()
|
|
|
|
expect(listener).not.toHaveBeenCalled()
|
|
validateIntegrityNoChanges()
|
|
|
|
// No links should have the same origin_id and target_id
|
|
for (const link of graph.links.values()) {
|
|
expect(link.origin_id).not.toEqual(link.target_id)
|
|
}
|
|
}
|
|
)
|
|
|
|
test.for(nodeReroutePairs)(
|
|
'Should ignore connections looping back to the origin node input from a reroute',
|
|
(
|
|
{ nodeId, rerouteId },
|
|
{ graph, connector, validateIntegrityNoChanges }
|
|
) => {
|
|
const listener = vi.fn()
|
|
connector.listenUntilReset('link-created', listener)
|
|
|
|
const node = graph.getNodeById(nodeId)!
|
|
const reroute = graph.getReroute(rerouteId)!
|
|
const inputPos = node.getInputPos(0)
|
|
const dropOnInputEvent = {
|
|
canvasX: inputPos[0],
|
|
canvasY: inputPos[1]
|
|
} as any
|
|
|
|
connector.dragFromReroute(graph, reroute)
|
|
connector.dropLinks(graph, dropOnInputEvent)
|
|
connector.reset()
|
|
|
|
expect(listener).not.toHaveBeenCalled()
|
|
validateIntegrityNoChanges()
|
|
|
|
// No links should have the same origin_id and target_id
|
|
for (const link of graph.links.values()) {
|
|
expect(link.origin_id).not.toEqual(link.target_id)
|
|
}
|
|
}
|
|
)
|
|
})
|