mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-01 22:09:55 +00:00
1068 lines
38 KiB
TypeScript
1068 lines
38 KiB
TypeScript
import type { CanvasPointerEvent } from "@/types/events"
|
|
|
|
import { afterEach, describe, expect, vi } from "vitest"
|
|
|
|
import { LinkConnector } from "@/canvas/LinkConnector"
|
|
import { LGraph, LGraphNode, LLink, Reroute, type RerouteId } from "@/litegraph"
|
|
|
|
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 ({}, 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)
|
|
}
|
|
})
|
|
})
|