mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-01 13:59:54 +00:00
Follow up on https://github.com/Comfy-Org/litegraph.js/pull/915 Filter out non-serializable widgets during node configuration. Current usage in frontend should not be affected as preview media widgets are mostly dynamically added at the end of widget list.
456 lines
15 KiB
TypeScript
456 lines
15 KiB
TypeScript
import { describe, expect } from "vitest"
|
|
|
|
import { LGraphNode, LiteGraph } from "@/litegraph"
|
|
import { LGraph } from "@/litegraph"
|
|
import { NodeInputSlot, NodeOutputSlot } from "@/NodeSlot"
|
|
|
|
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)
|
|
})
|
|
})
|
|
})
|