mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-01 13:59:54 +00:00
457 lines
15 KiB
TypeScript
457 lines
15 KiB
TypeScript
import { describe, expect } from "vitest"
|
|
|
|
import { LGraphNode, LiteGraph } from "@/litegraph"
|
|
import { LGraph } from "@/litegraph"
|
|
import { NodeInputSlot } from "@/node/NodeInputSlot"
|
|
import { NodeOutputSlot } from "@/node/NodeOutputSlot"
|
|
|
|
import { test } from "./testExtensions"
|
|
|
|
describe("LGraphNode", () => {
|
|
test("should serialize position/size correctly", () => {
|
|
const node = new LGraphNode("TestNode")
|
|
node.pos = [10, 10]
|
|
expect(node.pos).toEqual(new Float32Array([10, 10]))
|
|
expect(node.serialize().pos).toEqual([10, 10])
|
|
|
|
node.size = [100, 100]
|
|
expect(node.size).toEqual(new Float32Array([100, 100]))
|
|
expect(node.serialize().size).toEqual([100, 100])
|
|
})
|
|
|
|
test("should configure inputs correctly", () => {
|
|
const node = new LGraphNode("TestNode")
|
|
node.configure({
|
|
id: 0,
|
|
inputs: [{ name: "TestInput", type: "number", link: null }],
|
|
})
|
|
expect(node.inputs.length).toEqual(1)
|
|
expect(node.inputs[0].name).toEqual("TestInput")
|
|
expect(node.inputs[0].link).toEqual(null)
|
|
expect(node.inputs[0]).instanceOf(NodeInputSlot)
|
|
|
|
// Should not override existing inputs
|
|
node.configure({ id: 1 })
|
|
expect(node.id).toEqual(1)
|
|
expect(node.inputs.length).toEqual(1)
|
|
})
|
|
|
|
test("should configure outputs correctly", () => {
|
|
const node = new LGraphNode("TestNode")
|
|
node.configure({
|
|
id: 0,
|
|
outputs: [{ name: "TestOutput", type: "number", links: [] }],
|
|
})
|
|
expect(node.outputs.length).toEqual(1)
|
|
expect(node.outputs[0].name).toEqual("TestOutput")
|
|
expect(node.outputs[0].type).toEqual("number")
|
|
expect(node.outputs[0].links).toEqual([])
|
|
expect(node.outputs[0]).instanceOf(NodeOutputSlot)
|
|
|
|
// Should not override existing outputs
|
|
node.configure({ id: 1 })
|
|
expect(node.id).toEqual(1)
|
|
expect(node.outputs.length).toEqual(1)
|
|
})
|
|
|
|
describe("Disconnect I/O Slots", () => {
|
|
test("should disconnect input correctly", () => {
|
|
const node1 = new LGraphNode("SourceNode")
|
|
const node2 = new LGraphNode("TargetNode")
|
|
|
|
// Configure nodes with input/output slots
|
|
node1.configure({
|
|
id: 1,
|
|
outputs: [{ name: "Output1", type: "number", links: [] }],
|
|
})
|
|
node2.configure({
|
|
id: 2,
|
|
inputs: [{ name: "Input1", type: "number", link: null }],
|
|
})
|
|
|
|
// Create a graph and add nodes to it
|
|
const graph = new LGraph()
|
|
graph.add(node1)
|
|
graph.add(node2)
|
|
|
|
// Connect the nodes
|
|
const link = node1.connect(0, node2, 0)
|
|
expect(link).not.toBeNull()
|
|
expect(node2.inputs[0].link).toBe(link?.id)
|
|
expect(node1.outputs[0].links).toContain(link?.id)
|
|
|
|
// Test disconnecting by slot number
|
|
const disconnected = node2.disconnectInput(0)
|
|
expect(disconnected).toBe(true)
|
|
expect(node2.inputs[0].link).toBeNull()
|
|
expect(node1.outputs[0].links?.length).toBe(0)
|
|
expect(graph._links.has(link?.id ?? -1)).toBe(false)
|
|
|
|
// Test disconnecting by slot name
|
|
node1.connect(0, node2, 0)
|
|
const disconnectedByName = node2.disconnectInput("Input1")
|
|
expect(disconnectedByName).toBe(true)
|
|
expect(node2.inputs[0].link).toBeNull()
|
|
|
|
// Test disconnecting non-existent slot
|
|
const invalidDisconnect = node2.disconnectInput(999)
|
|
expect(invalidDisconnect).toBe(false)
|
|
|
|
// Test disconnecting already disconnected input
|
|
const alreadyDisconnected = node2.disconnectInput(0)
|
|
expect(alreadyDisconnected).toBe(true)
|
|
})
|
|
|
|
test("should disconnect output correctly", () => {
|
|
const sourceNode = new LGraphNode("SourceNode")
|
|
const targetNode1 = new LGraphNode("TargetNode1")
|
|
const targetNode2 = new LGraphNode("TargetNode2")
|
|
|
|
// Configure nodes with input/output slots
|
|
sourceNode.configure({
|
|
id: 1,
|
|
outputs: [
|
|
{ name: "Output1", type: "number", links: [] },
|
|
{ name: "Output2", type: "number", links: [] },
|
|
],
|
|
})
|
|
targetNode1.configure({
|
|
id: 2,
|
|
inputs: [{ name: "Input1", type: "number", link: null }],
|
|
})
|
|
targetNode2.configure({
|
|
id: 3,
|
|
inputs: [{ name: "Input1", type: "number", link: null }],
|
|
})
|
|
|
|
// Create a graph and add nodes to it
|
|
const graph = new LGraph()
|
|
graph.add(sourceNode)
|
|
graph.add(targetNode1)
|
|
graph.add(targetNode2)
|
|
|
|
// Connect multiple nodes to the same output
|
|
const link1 = sourceNode.connect(0, targetNode1, 0)
|
|
const link2 = sourceNode.connect(0, targetNode2, 0)
|
|
expect(link1).not.toBeNull()
|
|
expect(link2).not.toBeNull()
|
|
expect(sourceNode.outputs[0].links?.length).toBe(2)
|
|
|
|
// Test disconnecting specific target node
|
|
const disconnectedSpecific = sourceNode.disconnectOutput(0, targetNode1)
|
|
expect(disconnectedSpecific).toBe(true)
|
|
expect(targetNode1.inputs[0].link).toBeNull()
|
|
expect(sourceNode.outputs[0].links?.length).toBe(1)
|
|
expect(graph._links.has(link1?.id ?? -1)).toBe(false)
|
|
expect(graph._links.has(link2?.id ?? -1)).toBe(true)
|
|
|
|
// Test disconnecting by slot name
|
|
const link3 = sourceNode.connect(1, targetNode1, 0)
|
|
expect(link3).not.toBeNull()
|
|
const disconnectedByName = sourceNode.disconnectOutput("Output2", targetNode1)
|
|
expect(disconnectedByName).toBe(true)
|
|
expect(targetNode1.inputs[0].link).toBeNull()
|
|
expect(sourceNode.outputs[1].links?.length).toBe(0)
|
|
|
|
// Test disconnecting all connections from an output
|
|
const link4 = sourceNode.connect(0, targetNode1, 0)
|
|
expect(link4).not.toBeNull()
|
|
expect(sourceNode.outputs[0].links?.length).toBe(2)
|
|
const disconnectedAll = sourceNode.disconnectOutput(0)
|
|
expect(disconnectedAll).toBe(true)
|
|
expect(sourceNode.outputs[0].links).toBeNull()
|
|
expect(targetNode1.inputs[0].link).toBeNull()
|
|
expect(targetNode2.inputs[0].link).toBeNull()
|
|
expect(graph._links.has(link2?.id ?? -1)).toBe(false)
|
|
expect(graph._links.has(link4?.id ?? -1)).toBe(false)
|
|
|
|
// Test disconnecting non-existent slot
|
|
const invalidDisconnect = sourceNode.disconnectOutput(999)
|
|
expect(invalidDisconnect).toBe(false)
|
|
|
|
// Test disconnecting already disconnected output
|
|
const alreadyDisconnected = sourceNode.disconnectOutput(0)
|
|
expect(alreadyDisconnected).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("getInputPos and getOutputPos", () => {
|
|
test("should handle collapsed nodes correctly", () => {
|
|
const node = new LGraphNode("TestNode") as unknown as Omit<LGraphNode, "boundingRect"> & { boundingRect: Float32Array }
|
|
node.pos = [100, 100]
|
|
node.size = [100, 100]
|
|
node.boundingRect[0] = 100
|
|
node.boundingRect[1] = 100
|
|
node.boundingRect[2] = 100
|
|
node.boundingRect[3] = 100
|
|
node.configure({
|
|
id: 1,
|
|
inputs: [{ name: "Input1", type: "number", link: null }],
|
|
outputs: [{ name: "Output1", type: "number", links: [] }],
|
|
})
|
|
|
|
// Collapse the node
|
|
node.flags.collapsed = true
|
|
|
|
// Get positions in collapsed state
|
|
const inputPos = node.getInputPos(0)
|
|
const outputPos = node.getOutputPos(0)
|
|
|
|
expect(inputPos).toEqual([100, 85])
|
|
expect(outputPos).toEqual([180, 85])
|
|
})
|
|
|
|
test("should return correct positions for input and output slots", () => {
|
|
const node = new LGraphNode("TestNode")
|
|
node.pos = [100, 100]
|
|
node.size = [100, 100]
|
|
node.configure({
|
|
id: 1,
|
|
inputs: [{ name: "Input1", type: "number", link: null }],
|
|
outputs: [{ name: "Output1", type: "number", links: [] }],
|
|
})
|
|
|
|
const inputPos = node.getInputPos(0)
|
|
const outputPos = node.getOutputPos(0)
|
|
|
|
expect(inputPos).toEqual([110, 114])
|
|
expect(outputPos).toEqual([191, 114])
|
|
})
|
|
})
|
|
|
|
describe("getSlotOnPos", () => {
|
|
test("should return undefined when point is outside node bounds", () => {
|
|
const node = new LGraphNode("TestNode")
|
|
node.pos = [100, 100]
|
|
node.size = [100, 100]
|
|
node.configure({
|
|
id: 1,
|
|
inputs: [{ name: "Input1", type: "number", link: null }],
|
|
outputs: [{ name: "Output1", type: "number", links: [] }],
|
|
})
|
|
|
|
// Test point far outside node bounds
|
|
expect(node.getSlotOnPos([0, 0])).toBeUndefined()
|
|
// Test point just outside node bounds
|
|
expect(node.getSlotOnPos([99, 99])).toBeUndefined()
|
|
})
|
|
|
|
test("should detect input slots correctly", () => {
|
|
const node = new LGraphNode("TestNode") as unknown as Omit<LGraphNode, "boundingRect"> & { boundingRect: Float32Array }
|
|
node.pos = [100, 100]
|
|
node.size = [100, 100]
|
|
node.boundingRect[0] = 100
|
|
node.boundingRect[1] = 100
|
|
node.boundingRect[2] = 200
|
|
node.boundingRect[3] = 200
|
|
node.configure({
|
|
id: 1,
|
|
inputs: [
|
|
{ name: "Input1", type: "number", link: null },
|
|
{ name: "Input2", type: "string", link: null },
|
|
],
|
|
})
|
|
|
|
// Get position of first input slot
|
|
const inputPos = node.getInputPos(0)
|
|
// Test point directly on input slot
|
|
const slot = node.getSlotOnPos(inputPos)
|
|
expect(slot).toBeDefined()
|
|
expect(slot?.name).toBe("Input1")
|
|
|
|
// Test point near but not on input slot
|
|
expect(node.getSlotOnPos([inputPos[0] - 15, inputPos[1]])).toBeUndefined()
|
|
})
|
|
|
|
test("should detect output slots correctly", () => {
|
|
const node = new LGraphNode("TestNode") as unknown as Omit<LGraphNode, "boundingRect"> & { boundingRect: Float32Array }
|
|
node.pos = [100, 100]
|
|
node.size = [100, 100]
|
|
node.boundingRect[0] = 100
|
|
node.boundingRect[1] = 100
|
|
node.boundingRect[2] = 200
|
|
node.boundingRect[3] = 200
|
|
node.configure({
|
|
id: 1,
|
|
outputs: [
|
|
{ name: "Output1", type: "number", links: [] },
|
|
{ name: "Output2", type: "string", links: [] },
|
|
],
|
|
})
|
|
|
|
// Get position of first output slot
|
|
const outputPos = node.getOutputPos(0)
|
|
// Test point directly on output slot
|
|
const slot = node.getSlotOnPos(outputPos)
|
|
expect(slot).toBeDefined()
|
|
expect(slot?.name).toBe("Output1")
|
|
|
|
// Test point near but not on output slot
|
|
const gotslot = node.getSlotOnPos([outputPos[0] + 30, outputPos[1]])
|
|
expect(gotslot).toBeUndefined()
|
|
})
|
|
|
|
test("should prioritize input slots over output slots", () => {
|
|
const node = new LGraphNode("TestNode") as unknown as Omit<LGraphNode, "boundingRect"> & { boundingRect: Float32Array }
|
|
node.pos = [100, 100]
|
|
node.size = [100, 100]
|
|
node.boundingRect[0] = 100
|
|
node.boundingRect[1] = 100
|
|
node.boundingRect[2] = 200
|
|
node.boundingRect[3] = 200
|
|
node.configure({
|
|
id: 1,
|
|
inputs: [{ name: "Input1", type: "number", link: null }],
|
|
outputs: [{ name: "Output1", type: "number", links: [] }],
|
|
})
|
|
|
|
// Get positions of first input and output slots
|
|
const inputPos = node.getInputPos(0)
|
|
|
|
// Test point that could theoretically hit both slots
|
|
// Should return the input slot due to priority
|
|
const slot = node.getSlotOnPos(inputPos)
|
|
expect(slot).toBeDefined()
|
|
expect(slot?.name).toBe("Input1")
|
|
})
|
|
})
|
|
|
|
describe("LGraphNode slot positioning", () => {
|
|
test("should correctly position slots with absolute coordinates", () => {
|
|
// Setup
|
|
const node = new LGraphNode("test")
|
|
node.pos = [100, 100]
|
|
|
|
// Add input/output with absolute positions
|
|
node.addInput("abs-input", "number")
|
|
node.inputs[0].pos = [10, 20]
|
|
|
|
node.addOutput("abs-output", "number")
|
|
node.outputs[0].pos = [50, 30]
|
|
|
|
// Test
|
|
const inputPos = node.getInputPos(0)
|
|
const outputPos = node.getOutputPos(0)
|
|
|
|
// Absolute positions should be relative to node position
|
|
expect(inputPos).toEqual([110, 120]) // node.pos + slot.pos
|
|
expect(outputPos).toEqual([150, 130]) // node.pos + slot.pos
|
|
})
|
|
|
|
test("should correctly position default vertical slots", () => {
|
|
// Setup
|
|
const node = new LGraphNode("test")
|
|
node.pos = [100, 100]
|
|
|
|
// Add multiple inputs/outputs without absolute positions
|
|
node.addInput("input1", "number")
|
|
node.addInput("input2", "number")
|
|
node.addOutput("output1", "number")
|
|
node.addOutput("output2", "number")
|
|
|
|
// Calculate expected positions
|
|
const slotOffset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
|
|
const slotSpacing = LiteGraph.NODE_SLOT_HEIGHT
|
|
const nodeWidth = node.size[0]
|
|
|
|
// Test input positions
|
|
expect(node.getInputPos(0)).toEqual([
|
|
100 + slotOffset,
|
|
100 + (0 + 0.7) * slotSpacing,
|
|
])
|
|
expect(node.getInputPos(1)).toEqual([
|
|
100 + slotOffset,
|
|
100 + (1 + 0.7) * slotSpacing,
|
|
])
|
|
|
|
// Test output positions
|
|
expect(node.getOutputPos(0)).toEqual([
|
|
100 + nodeWidth + 1 - slotOffset,
|
|
100 + (0 + 0.7) * slotSpacing,
|
|
])
|
|
expect(node.getOutputPos(1)).toEqual([
|
|
100 + nodeWidth + 1 - slotOffset,
|
|
100 + (1 + 0.7) * slotSpacing,
|
|
])
|
|
})
|
|
|
|
test("should skip absolute positioned slots when calculating vertical positions", () => {
|
|
// Setup
|
|
const node = new LGraphNode("test")
|
|
node.pos = [100, 100]
|
|
|
|
// Add mix of absolute and default positioned slots
|
|
node.addInput("abs-input", "number")
|
|
node.inputs[0].pos = [10, 20]
|
|
node.addInput("default-input1", "number")
|
|
node.addInput("default-input2", "number")
|
|
|
|
const slotOffset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
|
|
const slotSpacing = LiteGraph.NODE_SLOT_HEIGHT
|
|
|
|
// Test: default positioned slots should be consecutive, ignoring absolute positioned ones
|
|
expect(node.getInputPos(1)).toEqual([
|
|
100 + slotOffset,
|
|
100 + (0 + 0.7) * slotSpacing, // First default slot starts at index 0
|
|
])
|
|
expect(node.getInputPos(2)).toEqual([
|
|
100 + slotOffset,
|
|
100 + (1 + 0.7) * slotSpacing, // Second default slot at index 1
|
|
])
|
|
})
|
|
})
|
|
|
|
describe("widget serialization", () => {
|
|
test("should only serialize widgets with serialize flag not set to false", () => {
|
|
const node = new LGraphNode("TestNode")
|
|
node.serialize_widgets = true
|
|
|
|
// Add widgets with different serialization settings
|
|
node.addWidget("number", "serializable1", 1, null)
|
|
node.addWidget("number", "serializable2", 2, null)
|
|
node.addWidget("number", "non-serializable", 3, null)
|
|
expect(node.widgets?.length).toBe(3)
|
|
|
|
// Set serialize flag to false for the last widget
|
|
node.widgets![2].serialize = false
|
|
|
|
// Set some widget values
|
|
node.widgets![0].value = 10
|
|
node.widgets![1].value = 20
|
|
node.widgets![2].value = 30
|
|
|
|
// Serialize the node
|
|
const serialized = node.serialize()
|
|
|
|
// Check that only serializable widgets' values are included
|
|
expect(serialized.widgets_values).toEqual([10, 20])
|
|
expect(serialized.widgets_values).toHaveLength(2)
|
|
})
|
|
|
|
test("should only configure widgets with serialize flag not set to false", () => {
|
|
const node = new LGraphNode("TestNode")
|
|
node.serialize_widgets = true
|
|
|
|
node.addWidget("number", "non-serializable", 1, null)
|
|
node.addWidget("number", "serializable1", 2, null)
|
|
expect(node.widgets?.length).toBe(2)
|
|
|
|
node.widgets![0].serialize = false
|
|
node.configure({
|
|
id: 1,
|
|
type: "TestNode",
|
|
pos: [100, 100],
|
|
size: [100, 100],
|
|
flags: {},
|
|
properties: {},
|
|
order: 0,
|
|
mode: 0,
|
|
widgets_values: [100],
|
|
})
|
|
|
|
expect(node.widgets![0].value).toBe(1)
|
|
expect(node.widgets![1].value).toBe(100)
|
|
})
|
|
})
|
|
})
|