mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-23 07:50:15 +00:00
[feat] Add subgraph test infrastructure (#1111)
This commit is contained in:
374
test/subgraph/Subgraph.test.ts
Normal file
374
test/subgraph/Subgraph.test.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
/**
|
||||
* Core Subgraph Tests
|
||||
*
|
||||
* Fundamental tests for the Subgraph class covering construction,
|
||||
* basic I/O management, and edge cases.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { RecursionError } from "@/infrastructure/RecursionError"
|
||||
import { LGraph, Subgraph } from "@/litegraph"
|
||||
import { createUuidv4 } from "@/utils/uuid"
|
||||
|
||||
import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
import {
|
||||
assertSubgraphStructure,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphData,
|
||||
verifyEventSequence,
|
||||
} from "./fixtures/subgraphHelpers"
|
||||
|
||||
describe("Subgraph Construction", () => {
|
||||
it("should create a subgraph with minimal data", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
assertSubgraphStructure(subgraph, {
|
||||
inputCount: 0,
|
||||
outputCount: 0,
|
||||
nodeCount: 0,
|
||||
name: "Test Subgraph",
|
||||
})
|
||||
|
||||
expect(subgraph.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
|
||||
expect(subgraph.inputNode).toBeDefined()
|
||||
expect(subgraph.outputNode).toBeDefined()
|
||||
expect(subgraph.inputNode.id).toBe(-10)
|
||||
expect(subgraph.outputNode.id).toBe(-20)
|
||||
})
|
||||
|
||||
it("should require a root graph", () => {
|
||||
const subgraphData = createTestSubgraphData()
|
||||
|
||||
expect(() => {
|
||||
// @ts-expect-error Testing invalid null parameter
|
||||
new Subgraph(null, subgraphData)
|
||||
}).toThrow("Root graph is required")
|
||||
})
|
||||
|
||||
it("should accept custom name and ID", () => {
|
||||
const customId = createUuidv4()
|
||||
const customName = "My Custom Subgraph"
|
||||
|
||||
const subgraph = createTestSubgraph({
|
||||
id: customId,
|
||||
name: customName,
|
||||
})
|
||||
|
||||
expect(subgraph.id).toBe(customId)
|
||||
expect(subgraph.name).toBe(customName)
|
||||
})
|
||||
|
||||
it("should initialize with empty inputs and outputs", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
expect(subgraph.inputs).toHaveLength(0)
|
||||
expect(subgraph.outputs).toHaveLength(0)
|
||||
expect(subgraph.widgets).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should have properly configured input and output nodes", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// Input node should be positioned on the left
|
||||
expect(subgraph.inputNode.pos[0]).toBeLessThan(100)
|
||||
|
||||
// Output node should be positioned on the right
|
||||
expect(subgraph.outputNode.pos[0]).toBeGreaterThan(300)
|
||||
|
||||
// Both should reference the subgraph
|
||||
expect(subgraph.inputNode.subgraph).toBe(subgraph)
|
||||
expect(subgraph.outputNode.subgraph).toBe(subgraph)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Subgraph Input/Output Management", () => {
|
||||
subgraphTest("should add a single input", ({ emptySubgraph }) => {
|
||||
const input = emptySubgraph.addInput("test_input", "number")
|
||||
|
||||
expect(emptySubgraph.inputs).toHaveLength(1)
|
||||
expect(input.name).toBe("test_input")
|
||||
expect(input.type).toBe("number")
|
||||
expect(emptySubgraph.inputs.indexOf(input)).toBe(0)
|
||||
})
|
||||
|
||||
subgraphTest("should add a single output", ({ emptySubgraph }) => {
|
||||
const output = emptySubgraph.addOutput("test_output", "string")
|
||||
|
||||
expect(emptySubgraph.outputs).toHaveLength(1)
|
||||
expect(output.name).toBe("test_output")
|
||||
expect(output.type).toBe("string")
|
||||
expect(emptySubgraph.outputs.indexOf(output)).toBe(0)
|
||||
})
|
||||
|
||||
subgraphTest("should maintain correct indices when adding multiple inputs", ({ emptySubgraph }) => {
|
||||
const input1 = emptySubgraph.addInput("input_1", "number")
|
||||
const input2 = emptySubgraph.addInput("input_2", "string")
|
||||
const input3 = emptySubgraph.addInput("input_3", "boolean")
|
||||
|
||||
expect(emptySubgraph.inputs.indexOf(input1)).toBe(0)
|
||||
expect(emptySubgraph.inputs.indexOf(input2)).toBe(1)
|
||||
expect(emptySubgraph.inputs.indexOf(input3)).toBe(2)
|
||||
expect(emptySubgraph.inputs).toHaveLength(3)
|
||||
})
|
||||
|
||||
subgraphTest("should maintain correct indices when adding multiple outputs", ({ emptySubgraph }) => {
|
||||
const output1 = emptySubgraph.addOutput("output_1", "number")
|
||||
const output2 = emptySubgraph.addOutput("output_2", "string")
|
||||
const output3 = emptySubgraph.addOutput("output_3", "boolean")
|
||||
|
||||
expect(emptySubgraph.outputs.indexOf(output1)).toBe(0)
|
||||
expect(emptySubgraph.outputs.indexOf(output2)).toBe(1)
|
||||
expect(emptySubgraph.outputs.indexOf(output3)).toBe(2)
|
||||
expect(emptySubgraph.outputs).toHaveLength(3)
|
||||
})
|
||||
|
||||
subgraphTest("should remove inputs correctly", ({ simpleSubgraph }) => {
|
||||
// Add a second input first
|
||||
simpleSubgraph.addInput("second_input", "string")
|
||||
expect(simpleSubgraph.inputs).toHaveLength(2)
|
||||
|
||||
// Remove the first input
|
||||
const firstInput = simpleSubgraph.inputs[0]
|
||||
simpleSubgraph.removeInput(firstInput)
|
||||
|
||||
expect(simpleSubgraph.inputs).toHaveLength(1)
|
||||
expect(simpleSubgraph.inputs[0].name).toBe("second_input")
|
||||
// Verify it's at index 0 in the array
|
||||
expect(simpleSubgraph.inputs.indexOf(simpleSubgraph.inputs[0])).toBe(0)
|
||||
})
|
||||
|
||||
subgraphTest("should remove outputs correctly", ({ simpleSubgraph }) => {
|
||||
// Add a second output first
|
||||
simpleSubgraph.addOutput("second_output", "string")
|
||||
expect(simpleSubgraph.outputs).toHaveLength(2)
|
||||
|
||||
// Remove the first output
|
||||
const firstOutput = simpleSubgraph.outputs[0]
|
||||
simpleSubgraph.removeOutput(firstOutput)
|
||||
|
||||
expect(simpleSubgraph.outputs).toHaveLength(1)
|
||||
expect(simpleSubgraph.outputs[0].name).toBe("second_output")
|
||||
// Verify it's at index 0 in the array
|
||||
expect(simpleSubgraph.outputs.indexOf(simpleSubgraph.outputs[0])).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Subgraph Event System", () => {
|
||||
subgraphTest("should fire events when adding inputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addInput("test_input", "number")
|
||||
|
||||
verifyEventSequence(capture.events, ["adding-input", "input-added"])
|
||||
|
||||
expect(capture.events[0].detail.name).toBe("test_input")
|
||||
expect(capture.events[0].detail.type).toBe("number")
|
||||
expect(capture.events[1].detail.input.name).toBe("test_input")
|
||||
})
|
||||
|
||||
subgraphTest("should fire events when adding outputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addOutput("test_output", "string")
|
||||
|
||||
verifyEventSequence(capture.events, ["adding-output", "output-added"])
|
||||
|
||||
expect(capture.events[0].detail.name).toBe("test_output")
|
||||
expect(capture.events[0].detail.type).toBe("string")
|
||||
expect(capture.events[1].detail.output.name).toBe("test_output")
|
||||
})
|
||||
|
||||
subgraphTest("should fire events when removing inputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
// Add an input first
|
||||
const input = subgraph.addInput("test_input", "number")
|
||||
capture.clear() // Clear the add events
|
||||
|
||||
// Remove the input
|
||||
subgraph.removeInput(input)
|
||||
|
||||
verifyEventSequence(capture.events, ["removing-input"])
|
||||
expect(capture.events[0].detail.input.name).toBe("test_input")
|
||||
expect(capture.events[0].detail.index).toBe(0)
|
||||
})
|
||||
|
||||
subgraphTest("should fire events when removing outputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
// Add an output first
|
||||
const output = subgraph.addOutput("test_output", "string")
|
||||
capture.clear() // Clear the add events
|
||||
|
||||
// Remove the output
|
||||
subgraph.removeOutput(output)
|
||||
|
||||
verifyEventSequence(capture.events, ["removing-output"])
|
||||
expect(capture.events[0].detail.output.name).toBe("test_output")
|
||||
expect(capture.events[0].detail.index).toBe(0)
|
||||
})
|
||||
|
||||
subgraphTest("should allow preventing input removal via event", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
// Add an input
|
||||
const input = subgraph.addInput("protected_input", "number")
|
||||
|
||||
// Add event listener that prevents removal
|
||||
subgraph.events.addEventListener("removing-input", (event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
capture.clear()
|
||||
|
||||
// Try to remove the input
|
||||
subgraph.removeInput(input)
|
||||
|
||||
// Input should still exist
|
||||
expect(subgraph.inputs).toHaveLength(1)
|
||||
expect(subgraph.inputs[0].name).toBe("protected_input")
|
||||
|
||||
// Event should have been fired but removal prevented
|
||||
expect(capture.events).toHaveLength(1)
|
||||
expect(capture.events[0].type).toBe("removing-input")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Subgraph Serialization", () => {
|
||||
subgraphTest("should serialize empty subgraph", ({ emptySubgraph }) => {
|
||||
const serialized = emptySubgraph.asSerialisable()
|
||||
|
||||
expect(serialized.version).toBe(1)
|
||||
expect(serialized.id).toBeTruthy()
|
||||
expect(serialized.name).toBe("Empty Test Subgraph")
|
||||
expect(serialized.inputs).toHaveLength(0)
|
||||
expect(serialized.outputs).toHaveLength(0)
|
||||
expect(serialized.nodes).toHaveLength(0)
|
||||
expect(typeof serialized.links).toBe("object")
|
||||
})
|
||||
|
||||
subgraphTest("should serialize subgraph with inputs and outputs", ({ simpleSubgraph }) => {
|
||||
const serialized = simpleSubgraph.asSerialisable()
|
||||
|
||||
expect(serialized.inputs).toHaveLength(1)
|
||||
expect(serialized.outputs).toHaveLength(1)
|
||||
expect(serialized.inputs[0].name).toBe("input")
|
||||
expect(serialized.inputs[0].type).toBe("number")
|
||||
expect(serialized.outputs[0].name).toBe("output")
|
||||
expect(serialized.outputs[0].type).toBe("number")
|
||||
})
|
||||
|
||||
subgraphTest("should include input and output nodes in serialization", ({ emptySubgraph }) => {
|
||||
const serialized = emptySubgraph.asSerialisable()
|
||||
|
||||
expect(serialized.inputNode).toBeDefined()
|
||||
expect(serialized.outputNode).toBeDefined()
|
||||
expect(serialized.inputNode.id).toBe(-10)
|
||||
expect(serialized.outputNode.id).toBe(-20)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Subgraph Known Issues", () => {
|
||||
it("should provide MAX_NESTED_SUBGRAPHS constant", () => {
|
||||
expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBe(1000)
|
||||
})
|
||||
|
||||
it("should have recursion detection in place", () => {
|
||||
// Verify that RecursionError is available and can be thrown
|
||||
expect(() => {
|
||||
throw new RecursionError("test recursion")
|
||||
}).toThrow(RecursionError)
|
||||
|
||||
expect(() => {
|
||||
throw new RecursionError("test recursion")
|
||||
}).toThrow("test recursion")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Subgraph Root Graph Relationship", () => {
|
||||
it("should maintain reference to root graph", () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraphData = createTestSubgraphData()
|
||||
const subgraph = new Subgraph(rootGraph, subgraphData)
|
||||
|
||||
expect(subgraph.rootGraph).toBe(rootGraph)
|
||||
})
|
||||
|
||||
it("should inherit root graph in nested subgraphs", () => {
|
||||
const rootGraph = new LGraph()
|
||||
const parentData = createTestSubgraphData({
|
||||
name: "Parent Subgraph",
|
||||
})
|
||||
const parentSubgraph = new Subgraph(rootGraph, parentData)
|
||||
|
||||
// Create a nested subgraph
|
||||
const nestedData = createTestSubgraphData({
|
||||
name: "Nested Subgraph",
|
||||
})
|
||||
const nestedSubgraph = new Subgraph(rootGraph, nestedData)
|
||||
|
||||
expect(nestedSubgraph.rootGraph).toBe(rootGraph)
|
||||
expect(parentSubgraph.rootGraph).toBe(rootGraph)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Subgraph Error Handling", () => {
|
||||
subgraphTest("should handle removing non-existent input gracefully", ({ emptySubgraph }) => {
|
||||
// Create a fake input that doesn't belong to this subgraph
|
||||
const fakeInput = emptySubgraph.addInput("temp", "number")
|
||||
emptySubgraph.removeInput(fakeInput) // Remove it first
|
||||
|
||||
// Now try to remove it again
|
||||
expect(() => {
|
||||
emptySubgraph.removeInput(fakeInput)
|
||||
}).toThrow("Input not found")
|
||||
})
|
||||
|
||||
subgraphTest("should handle removing non-existent output gracefully", ({ emptySubgraph }) => {
|
||||
// Create a fake output that doesn't belong to this subgraph
|
||||
const fakeOutput = emptySubgraph.addOutput("temp", "number")
|
||||
emptySubgraph.removeOutput(fakeOutput) // Remove it first
|
||||
|
||||
// Now try to remove it again
|
||||
expect(() => {
|
||||
emptySubgraph.removeOutput(fakeOutput)
|
||||
}).toThrow("Output not found")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Subgraph Integration", () => {
|
||||
it("should work with LGraph's node management", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
nodeCount: 3,
|
||||
})
|
||||
|
||||
// Verify nodes were added to the subgraph
|
||||
expect(subgraph.nodes).toHaveLength(3)
|
||||
|
||||
// Verify we can access nodes by ID
|
||||
const firstNode = subgraph.getNodeById(1)
|
||||
expect(firstNode).toBeDefined()
|
||||
expect(firstNode?.title).toContain("Test Node")
|
||||
})
|
||||
|
||||
it("should maintain link integrity", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
nodeCount: 2,
|
||||
})
|
||||
|
||||
const node1 = subgraph.nodes[0]
|
||||
const node2 = subgraph.nodes[1]
|
||||
|
||||
// Connect the nodes
|
||||
node1.connect(0, node2, 0)
|
||||
|
||||
// Verify link was created
|
||||
expect(subgraph.links.size).toBe(1)
|
||||
|
||||
// Verify link integrity
|
||||
const link = Array.from(subgraph.links.values())[0]
|
||||
expect(link.origin_id).toBe(node1.id)
|
||||
expect(link.target_id).toBe(node2.id)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user