mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-04 23:20:07 +00:00
[feat] Add subgraph test infrastructure (#1111)
This commit is contained in:
@@ -5,7 +5,7 @@ import type { ExportedSubgraph, ExposedWidget, ISerialisedGraph, Serialisable, S
|
||||
|
||||
import { CustomEventTarget } from "@/infrastructure/CustomEventTarget"
|
||||
import { type BaseLGraph, LGraph } from "@/LGraph"
|
||||
import { createUuidv4 } from "@/litegraph"
|
||||
import { createUuidv4 } from "@/utils/uuid"
|
||||
|
||||
import { SubgraphInput } from "./SubgraphInput"
|
||||
import { SubgraphInputNode } from "./SubgraphInputNode"
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
99
test/subgraph/SubgraphNode.test.ts
Normal file
99
test/subgraph/SubgraphNode.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* SubgraphNode Tests
|
||||
*
|
||||
* Tests for SubgraphNode instances including construction,
|
||||
* IO synchronization, and edge cases.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { LGraph } from "@/litegraph"
|
||||
|
||||
import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
} from "./fixtures/subgraphHelpers"
|
||||
|
||||
describe("SubgraphNode Construction", () => {
|
||||
it("should create a SubgraphNode from a subgraph definition", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Test Definition",
|
||||
inputs: [{ name: "input", type: "number" }],
|
||||
outputs: [{ name: "output", type: "number" }],
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
expect(subgraphNode).toBeDefined()
|
||||
expect(subgraphNode.subgraph).toBe(subgraph)
|
||||
expect(subgraphNode.type).toBe(subgraph.id)
|
||||
expect(subgraphNode.isVirtualNode).toBe(true)
|
||||
})
|
||||
|
||||
subgraphTest("should synchronize slots with subgraph definition", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode } = subgraphWithNode
|
||||
|
||||
// SubgraphNode should have same number of inputs/outputs as definition
|
||||
expect(subgraphNode.inputs).toHaveLength(subgraph.inputs.length)
|
||||
expect(subgraphNode.outputs).toHaveLength(subgraph.outputs.length)
|
||||
})
|
||||
|
||||
subgraphTest("should update slots when subgraph definition changes", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode } = subgraphWithNode
|
||||
|
||||
const initialInputCount = subgraphNode.inputs.length
|
||||
|
||||
// Add an input to the subgraph definition
|
||||
subgraph.addInput("new_input", "string")
|
||||
|
||||
// SubgraphNode should automatically update (this tests the event system)
|
||||
expect(subgraphNode.inputs).toHaveLength(initialInputCount + 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphNode Integration", () => {
|
||||
it("should be addable to a parent graph", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const parentGraph = new LGraph()
|
||||
|
||||
parentGraph.add(subgraphNode)
|
||||
|
||||
expect(parentGraph.nodes).toContain(subgraphNode)
|
||||
expect(subgraphNode.graph).toBe(parentGraph)
|
||||
})
|
||||
|
||||
subgraphTest("should maintain reference to root graph", ({ subgraphWithNode }) => {
|
||||
const { subgraphNode } = subgraphWithNode
|
||||
|
||||
// For this test, parentGraph should be the root, but in nested scenarios
|
||||
// it would traverse up to find the actual root
|
||||
expect(subgraphNode.rootGraph).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Foundation Test Utilities", () => {
|
||||
it("should create test SubgraphNodes with custom options", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const customPos: [number, number] = [500, 300]
|
||||
const customSize: [number, number] = [250, 120]
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, {
|
||||
pos: customPos,
|
||||
size: customSize,
|
||||
})
|
||||
|
||||
expect(Array.from(subgraphNode.pos)).toEqual(customPos)
|
||||
expect(Array.from(subgraphNode.size)).toEqual(customSize)
|
||||
})
|
||||
|
||||
subgraphTest("fixtures should provide properly configured SubgraphNode", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
|
||||
expect(subgraph).toBeDefined()
|
||||
expect(subgraphNode).toBeDefined()
|
||||
expect(parentGraph).toBeDefined()
|
||||
expect(parentGraph.nodes).toContain(subgraphNode)
|
||||
})
|
||||
})
|
||||
239
test/subgraph/fixtures/README.md
Normal file
239
test/subgraph/fixtures/README.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# Subgraph Testing Fixtures and Utilities
|
||||
|
||||
Testing infrastructure for LiteGraph's subgraph functionality. A subgraph is a graph-within-a-graph that can be reused as a single node, with input/output slots mapping to internal IO nodes.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
// Import what you need
|
||||
import { createTestSubgraph, assertSubgraphStructure } from "./fixtures/subgraphHelpers"
|
||||
import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
|
||||
// Option 1: Create a subgraph manually
|
||||
it("should do something", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "My Test Subgraph",
|
||||
inputCount: 2,
|
||||
outputCount: 1
|
||||
})
|
||||
|
||||
// Test your functionality
|
||||
expect(subgraph.inputs).toHaveLength(2)
|
||||
})
|
||||
|
||||
// Option 2: Use pre-configured fixtures
|
||||
subgraphTest("should handle events", ({ simpleSubgraph }) => {
|
||||
// simpleSubgraph comes pre-configured with 1 input, 1 output, and 2 nodes
|
||||
expect(simpleSubgraph.inputs).toHaveLength(1)
|
||||
})
|
||||
```
|
||||
|
||||
## Files Overview
|
||||
|
||||
### `subgraphHelpers.ts` - Core Helper Functions
|
||||
|
||||
**Main Factory Functions:**
|
||||
- `createTestSubgraph(options?)` - Creates a fully configured Subgraph instance with root graph
|
||||
- `createTestSubgraphNode(subgraph, options?)` - Creates a SubgraphNode (instance of a subgraph)
|
||||
- `createNestedSubgraphs(options?)` - Creates nested subgraph hierarchies for testing deep structures
|
||||
|
||||
**Assertion & Validation:**
|
||||
- `assertSubgraphStructure(subgraph, expected)` - Validates subgraph has expected inputs/outputs/nodes
|
||||
- `verifyEventSequence(events, expectedSequence)` - Ensures events fired in correct order
|
||||
- `logSubgraphStructure(subgraph, label?)` - Debug helper to print subgraph structure
|
||||
|
||||
**Test Data & Events:**
|
||||
- `createTestSubgraphData(overrides?)` - Creates raw ExportedSubgraph data for serialization tests
|
||||
- `createComplexSubgraphData(nodeCount?)` - Generates complex subgraph with internal connections
|
||||
- `createEventCapture(eventTarget, eventTypes)` - Sets up event monitoring with automatic cleanup
|
||||
|
||||
### `subgraphFixtures.ts` - Vitest Fixtures
|
||||
|
||||
Pre-configured test scenarios that automatically set up and tear down:
|
||||
|
||||
**Basic Fixtures (`subgraphTest`):**
|
||||
- `emptySubgraph` - Minimal subgraph with no inputs/outputs/nodes
|
||||
- `simpleSubgraph` - 1 input ("input": number), 1 output ("output": number), 2 internal nodes
|
||||
- `complexSubgraph` - 3 inputs (data, control, text), 2 outputs (result, status), 5 nodes
|
||||
- `nestedSubgraph` - 3-level deep hierarchy with 2 nodes per level
|
||||
- `subgraphWithNode` - Complete setup: subgraph definition + SubgraphNode instance + parent graph
|
||||
- `eventCapture` - Subgraph with event monitoring for all I/O events
|
||||
|
||||
**Edge Case Fixtures (`edgeCaseTest`):**
|
||||
- `circularSubgraph` - Two subgraphs set up for circular reference testing
|
||||
- `deeplyNestedSubgraph` - 50 levels deep for performance/limit testing
|
||||
- `maxIOSubgraph` - 20 inputs and 20 outputs for stress testing
|
||||
|
||||
### `testSubgraphs.json` - Sample Test Data
|
||||
Pre-defined subgraph configurations for consistent testing across different scenarios.
|
||||
|
||||
**Note on Static UUIDs**: The hardcoded UUIDs in this file (e.g., "simple-subgraph-uuid", "complex-subgraph-uuid") are intentionally static to ensure test reproducibility and snapshot testing compatibility.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Test Creation
|
||||
|
||||
```typescript
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { createTestSubgraph, assertSubgraphStructure } from "./fixtures/subgraphHelpers"
|
||||
|
||||
describe("My Subgraph Feature", () => {
|
||||
it("should work correctly", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "My Test",
|
||||
inputCount: 2,
|
||||
outputCount: 1,
|
||||
nodeCount: 3
|
||||
})
|
||||
|
||||
assertSubgraphStructure(subgraph, {
|
||||
inputCount: 2,
|
||||
outputCount: 1,
|
||||
nodeCount: 3,
|
||||
name: "My Test"
|
||||
})
|
||||
|
||||
// Your specific test logic...
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Using Fixtures
|
||||
|
||||
```typescript
|
||||
import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
|
||||
subgraphTest("should handle events", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addInput("test", "number")
|
||||
|
||||
expect(capture.events).toHaveLength(2) // adding-input, input-added
|
||||
})
|
||||
```
|
||||
|
||||
### Event Testing
|
||||
|
||||
```typescript
|
||||
import { createEventCapture, verifyEventSequence } from "./fixtures/subgraphHelpers"
|
||||
|
||||
it("should fire events in correct order", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const capture = createEventCapture(subgraph.events, ["adding-input", "input-added"])
|
||||
|
||||
subgraph.addInput("test", "number")
|
||||
|
||||
verifyEventSequence(capture.events, ["adding-input", "input-added"])
|
||||
|
||||
capture.cleanup() // Important: clean up listeners
|
||||
})
|
||||
```
|
||||
|
||||
### Nested Structure Testing
|
||||
|
||||
```typescript
|
||||
import { createNestedSubgraphs } from "./fixtures/subgraphHelpers"
|
||||
|
||||
it("should handle deep nesting", () => {
|
||||
const nested = createNestedSubgraphs({
|
||||
depth: 5,
|
||||
nodesPerLevel: 2
|
||||
})
|
||||
|
||||
expect(nested.subgraphs).toHaveLength(5)
|
||||
expect(nested.leafSubgraph.nodes).toHaveLength(2)
|
||||
})
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Testing SubgraphNode Instances
|
||||
|
||||
```typescript
|
||||
it("should create and configure a SubgraphNode", () => {
|
||||
// First create the subgraph definition
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "value", type: "number" }],
|
||||
outputs: [{ name: "result", type: "number" }]
|
||||
})
|
||||
|
||||
// Then create an instance of it
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, {
|
||||
pos: [100, 200],
|
||||
size: [180, 100]
|
||||
})
|
||||
|
||||
// The SubgraphNode will have matching slots
|
||||
expect(subgraphNode.inputs).toHaveLength(1)
|
||||
expect(subgraphNode.outputs).toHaveLength(1)
|
||||
expect(subgraphNode.subgraph).toBe(subgraph)
|
||||
})
|
||||
```
|
||||
|
||||
### Complete Test with Parent Graph
|
||||
|
||||
```typescript
|
||||
subgraphTest("should work in a parent graph", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
|
||||
// Everything is pre-configured and connected
|
||||
expect(parentGraph.nodes).toContain(subgraphNode)
|
||||
expect(subgraphNode.graph).toBe(parentGraph)
|
||||
expect(subgraphNode.subgraph).toBe(subgraph)
|
||||
})
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### `createTestSubgraph(options)`
|
||||
```typescript
|
||||
interface TestSubgraphOptions {
|
||||
id?: UUID // Custom UUID
|
||||
name?: string // Custom name
|
||||
nodeCount?: number // Number of internal nodes
|
||||
inputCount?: number // Number of inputs (uses generic types)
|
||||
outputCount?: number // Number of outputs (uses generic types)
|
||||
inputs?: Array<{ // Specific input definitions
|
||||
name: string
|
||||
type: ISlotType
|
||||
}>
|
||||
outputs?: Array<{ // Specific output definitions
|
||||
name: string
|
||||
type: ISlotType
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Cannot specify both `inputs` array and `inputCount` (or `outputs` array and `outputCount`) - the function will throw an error with details.
|
||||
|
||||
### `createNestedSubgraphs(options)`
|
||||
```typescript
|
||||
interface NestedSubgraphOptions {
|
||||
depth?: number // Nesting depth (default: 2)
|
||||
nodesPerLevel?: number // Nodes per subgraph (default: 2)
|
||||
inputsPerSubgraph?: number // Inputs per subgraph (default: 1)
|
||||
outputsPerSubgraph?: number // Outputs per subgraph (default: 1)
|
||||
}
|
||||
```
|
||||
|
||||
## Important Architecture Notes
|
||||
|
||||
### Subgraph vs SubgraphNode
|
||||
- **Subgraph**: The definition/template (like a class definition)
|
||||
- **SubgraphNode**: An instance of a subgraph placed in a graph (like a class instance)
|
||||
- One Subgraph can have many SubgraphNode instances
|
||||
|
||||
### Special Node IDs
|
||||
- Input node always has ID `-10` (SUBGRAPH_INPUT_ID)
|
||||
- Output node always has ID `-20` (SUBGRAPH_OUTPUT_ID)
|
||||
- These are virtual nodes that exist in every subgraph
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. **Array items don't have index property** - Use `indexOf()` instead
|
||||
2. **IO nodes have `subgraph` property** - Not `graph` like regular nodes
|
||||
3. **Links are stored in a Map** - Use `.size` not `.length`
|
||||
4. **Event detail structures** - Check exact property names:
|
||||
- `"adding-input"`: `{ name, type }`
|
||||
- `"input-added"`: `{ input, index }`
|
||||
|
||||
293
test/subgraph/fixtures/subgraphFixtures.ts
Normal file
293
test/subgraph/fixtures/subgraphFixtures.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Vitest Fixtures for Subgraph Testing
|
||||
*
|
||||
* Reusable Vitest fixtures for subgraph testing.
|
||||
* Each fixture provides a clean, pre-configured subgraph
|
||||
* setup for different testing scenarios.
|
||||
*/
|
||||
|
||||
import { LGraph, Subgraph } from "@/litegraph"
|
||||
import { SubgraphNode } from "@/subgraph/SubgraphNode"
|
||||
|
||||
import { test } from "../../testExtensions"
|
||||
import {
|
||||
createEventCapture,
|
||||
createNestedSubgraphs,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
} from "./subgraphHelpers"
|
||||
|
||||
export interface SubgraphFixtures {
|
||||
/** A minimal subgraph with no inputs, outputs, or nodes */
|
||||
emptySubgraph: Subgraph
|
||||
|
||||
/** A simple subgraph with 1 input and 1 output */
|
||||
simpleSubgraph: Subgraph
|
||||
|
||||
/** A complex subgraph with multiple inputs, outputs, and internal nodes */
|
||||
complexSubgraph: Subgraph
|
||||
|
||||
/** A nested subgraph structure (3 levels deep) */
|
||||
nestedSubgraph: ReturnType<typeof createNestedSubgraphs>
|
||||
|
||||
/** A subgraph with its corresponding SubgraphNode instance */
|
||||
subgraphWithNode: {
|
||||
subgraph: Subgraph
|
||||
subgraphNode: SubgraphNode
|
||||
parentGraph: LGraph
|
||||
}
|
||||
|
||||
/** Event capture system for testing subgraph events */
|
||||
eventCapture: {
|
||||
subgraph: Subgraph
|
||||
capture: ReturnType<typeof createEventCapture>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended test with subgraph fixtures.
|
||||
* Use this instead of the base `test` for subgraph testing.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
*
|
||||
* subgraphTest("should handle simple operations", ({ simpleSubgraph }) => {
|
||||
* expect(simpleSubgraph.inputs.length).toBe(1)
|
||||
* expect(simpleSubgraph.outputs.length).toBe(1)
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const subgraphTest = test.extend<SubgraphFixtures>({
|
||||
|
||||
emptySubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Empty Test Subgraph",
|
||||
inputCount: 0,
|
||||
outputCount: 0,
|
||||
nodeCount: 0,
|
||||
})
|
||||
|
||||
await use(subgraph)
|
||||
},
|
||||
|
||||
simpleSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Simple Test Subgraph",
|
||||
inputs: [{ name: "input", type: "number" }],
|
||||
outputs: [{ name: "output", type: "number" }],
|
||||
nodeCount: 2,
|
||||
})
|
||||
|
||||
await use(subgraph)
|
||||
},
|
||||
|
||||
complexSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Complex Test Subgraph",
|
||||
inputs: [
|
||||
{ name: "data", type: "number" },
|
||||
{ name: "control", type: "boolean" },
|
||||
{ name: "text", type: "string" },
|
||||
],
|
||||
outputs: [
|
||||
{ name: "result", type: "number" },
|
||||
{ name: "status", type: "boolean" },
|
||||
],
|
||||
nodeCount: 5,
|
||||
})
|
||||
|
||||
await use(subgraph)
|
||||
},
|
||||
|
||||
nestedSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
const nested = createNestedSubgraphs({
|
||||
depth: 3,
|
||||
nodesPerLevel: 2,
|
||||
inputsPerSubgraph: 1,
|
||||
outputsPerSubgraph: 1,
|
||||
})
|
||||
|
||||
await use(nested)
|
||||
},
|
||||
|
||||
subgraphWithNode: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
// Create the subgraph definition
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Subgraph With Node",
|
||||
inputs: [{ name: "input", type: "*" }],
|
||||
outputs: [{ name: "output", type: "*" }],
|
||||
nodeCount: 1,
|
||||
})
|
||||
|
||||
// Create the parent graph and subgraph node instance
|
||||
const parentGraph = new LGraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, {
|
||||
pos: [200, 200],
|
||||
size: [180, 80],
|
||||
})
|
||||
|
||||
// Add the subgraph node to the parent graph
|
||||
parentGraph.add(subgraphNode)
|
||||
|
||||
await use({
|
||||
subgraph,
|
||||
subgraphNode,
|
||||
parentGraph,
|
||||
})
|
||||
},
|
||||
|
||||
eventCapture: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Event Test Subgraph",
|
||||
})
|
||||
|
||||
// Set up event capture for all subgraph events
|
||||
const capture = createEventCapture(subgraph.events, [
|
||||
"adding-input",
|
||||
"input-added",
|
||||
"removing-input",
|
||||
"renaming-input",
|
||||
"adding-output",
|
||||
"output-added",
|
||||
"removing-output",
|
||||
"renaming-output",
|
||||
])
|
||||
|
||||
await use({ subgraph, capture })
|
||||
|
||||
// Cleanup event listeners
|
||||
capture.cleanup()
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Fixtures that test edge cases and error conditions.
|
||||
* These may leave the system in an invalid state and should be used carefully.
|
||||
*/
|
||||
export interface EdgeCaseFixtures {
|
||||
/** Subgraph with circular references (for testing recursion detection) */
|
||||
circularSubgraph: {
|
||||
rootGraph: LGraph
|
||||
subgraphA: Subgraph
|
||||
subgraphB: Subgraph
|
||||
nodeA: SubgraphNode
|
||||
nodeB: SubgraphNode
|
||||
}
|
||||
|
||||
/** Deeply nested subgraphs approaching the theoretical limit */
|
||||
deeplyNestedSubgraph: ReturnType<typeof createNestedSubgraphs>
|
||||
|
||||
/** Subgraph with maximum inputs and outputs */
|
||||
maxIOSubgraph: Subgraph
|
||||
}
|
||||
|
||||
/**
|
||||
* Test with edge case fixtures. Use sparingly and with caution.
|
||||
* These tests may intentionally create invalid states.
|
||||
*/
|
||||
export const edgeCaseTest = subgraphTest.extend<EdgeCaseFixtures>({
|
||||
|
||||
circularSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
const rootGraph = new LGraph()
|
||||
|
||||
// Create two subgraphs that will reference each other
|
||||
const subgraphA = createTestSubgraph({
|
||||
name: "Subgraph A",
|
||||
inputs: [{ name: "input", type: "*" }],
|
||||
outputs: [{ name: "output", type: "*" }],
|
||||
})
|
||||
|
||||
const subgraphB = createTestSubgraph({
|
||||
name: "Subgraph B",
|
||||
inputs: [{ name: "input", type: "*" }],
|
||||
outputs: [{ name: "output", type: "*" }],
|
||||
})
|
||||
|
||||
// Create instances (this doesn't create circular refs by itself)
|
||||
const nodeA = createTestSubgraphNode(subgraphA, { pos: [100, 100] })
|
||||
const nodeB = createTestSubgraphNode(subgraphB, { pos: [300, 100] })
|
||||
|
||||
// Add nodes to root graph
|
||||
rootGraph.add(nodeA)
|
||||
rootGraph.add(nodeB)
|
||||
|
||||
await use({
|
||||
rootGraph,
|
||||
subgraphA,
|
||||
subgraphB,
|
||||
nodeA,
|
||||
nodeB,
|
||||
})
|
||||
},
|
||||
|
||||
deeplyNestedSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
// Create a very deep nesting structure (but not exceeding MAX_NESTED_SUBGRAPHS)
|
||||
const nested = createNestedSubgraphs({
|
||||
depth: 50, // Deep but reasonable
|
||||
nodesPerLevel: 1,
|
||||
inputsPerSubgraph: 1,
|
||||
outputsPerSubgraph: 1,
|
||||
})
|
||||
|
||||
await use(nested)
|
||||
},
|
||||
|
||||
maxIOSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
// Create a subgraph with many inputs and outputs
|
||||
const inputs = Array.from({ length: 20 }, (_, i) => ({
|
||||
name: `input_${i}`,
|
||||
type: i % 2 === 0 ? "number" : "string" as const,
|
||||
}))
|
||||
|
||||
const outputs = Array.from({ length: 20 }, (_, i) => ({
|
||||
name: `output_${i}`,
|
||||
type: i % 2 === 0 ? "number" : "string" as const,
|
||||
}))
|
||||
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Max IO Subgraph",
|
||||
inputs,
|
||||
outputs,
|
||||
nodeCount: 10,
|
||||
})
|
||||
|
||||
await use(subgraph)
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Helper to verify fixture integrity.
|
||||
* Use this in tests to ensure fixtures are properly set up.
|
||||
*/
|
||||
export function verifyFixtureIntegrity<T extends Record<string, unknown>>(
|
||||
fixture: T,
|
||||
expectedProperties: (keyof T)[],
|
||||
): void {
|
||||
for (const prop of expectedProperties) {
|
||||
if (!(prop in fixture)) {
|
||||
throw new Error(`Fixture missing required property: ${String(prop)}`)
|
||||
}
|
||||
if (fixture[prop] === undefined || fixture[prop] === null) {
|
||||
throw new Error(`Fixture property ${String(prop)} is null or undefined`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a snapshot-friendly representation of a subgraph for testing.
|
||||
* Useful for serialization tests and regression detection.
|
||||
*/
|
||||
export function createSubgraphSnapshot(subgraph: Subgraph) {
|
||||
return {
|
||||
id: subgraph.id,
|
||||
name: subgraph.name,
|
||||
inputCount: subgraph.inputs.length,
|
||||
outputCount: subgraph.outputs.length,
|
||||
nodeCount: subgraph.nodes.length,
|
||||
linkCount: subgraph.links.size,
|
||||
inputs: subgraph.inputs.map(i => ({ name: i.name, type: i.type })),
|
||||
outputs: subgraph.outputs.map(o => ({ name: o.name, type: o.type })),
|
||||
hasInputNode: !!subgraph.inputNode,
|
||||
hasOutputNode: !!subgraph.outputNode,
|
||||
}
|
||||
}
|
||||
487
test/subgraph/fixtures/subgraphHelpers.ts
Normal file
487
test/subgraph/fixtures/subgraphHelpers.ts
Normal file
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* Test Helper Functions for Subgraph Testing
|
||||
*
|
||||
* Core utilities for creating and testing subgraphs.
|
||||
* Provides consistent APIs for test subgraph creation, node management,
|
||||
* and behavior verification.
|
||||
*/
|
||||
|
||||
import type { ISlotType, NodeId } from "@/litegraph"
|
||||
import type { ExportedSubgraph, ExportedSubgraphInstance } from "@/types/serialisation"
|
||||
import type { UUID } from "@/utils/uuid"
|
||||
|
||||
import { expect } from "vitest"
|
||||
|
||||
import { LGraph, LGraphNode, Subgraph } from "@/litegraph"
|
||||
import { SubgraphNode } from "@/subgraph/SubgraphNode"
|
||||
import { createUuidv4 } from "@/utils/uuid"
|
||||
|
||||
export interface TestSubgraphOptions {
|
||||
id?: UUID
|
||||
name?: string
|
||||
nodeCount?: number
|
||||
inputCount?: number
|
||||
outputCount?: number
|
||||
inputs?: Array<{ name: string, type: ISlotType }>
|
||||
outputs?: Array<{ name: string, type: ISlotType }>
|
||||
}
|
||||
|
||||
export interface TestSubgraphNodeOptions {
|
||||
id?: NodeId
|
||||
pos?: [number, number]
|
||||
size?: [number, number]
|
||||
}
|
||||
|
||||
export interface NestedSubgraphOptions {
|
||||
depth?: number
|
||||
nodesPerLevel?: number
|
||||
inputsPerSubgraph?: number
|
||||
outputsPerSubgraph?: number
|
||||
}
|
||||
|
||||
export interface SubgraphStructureExpectation {
|
||||
inputCount?: number
|
||||
outputCount?: number
|
||||
nodeCount?: number
|
||||
name?: string
|
||||
hasInputNode?: boolean
|
||||
hasOutputNode?: boolean
|
||||
}
|
||||
|
||||
export interface CapturedEvent<T = unknown> {
|
||||
type: string
|
||||
detail: T
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a test subgraph with the specified configuration.
|
||||
* This is the primary function for creating test subgraphs.
|
||||
* @param options Configuration options for the subgraph
|
||||
* @returns A configured Subgraph instance
|
||||
* @example
|
||||
* ```typescript
|
||||
* const subgraph = createTestSubgraph({
|
||||
* name: "My Test Subgraph",
|
||||
* inputCount: 2,
|
||||
* outputCount: 1
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph {
|
||||
// Validate options - cannot specify both inputs array and inputCount
|
||||
if (options.inputs && options.inputCount) {
|
||||
throw new Error(`Cannot specify both 'inputs' array and 'inputCount'. Choose one approach. Received options: ${JSON.stringify(options)}`)
|
||||
}
|
||||
|
||||
// Validate options - cannot specify both outputs array and outputCount
|
||||
if (options.outputs && options.outputCount) {
|
||||
throw new Error(`Cannot specify both 'outputs' array and 'outputCount'. Choose one approach. Received options: ${JSON.stringify(options)}`)
|
||||
}
|
||||
|
||||
const rootGraph = new LGraph()
|
||||
|
||||
// Create the base subgraph data
|
||||
const subgraphData: ExportedSubgraph = {
|
||||
// Basic graph properties
|
||||
version: 1,
|
||||
nodes: [],
|
||||
links: {},
|
||||
groups: [],
|
||||
config: {},
|
||||
definitions: { subgraphs: [] },
|
||||
|
||||
// Subgraph-specific properties
|
||||
id: options.id || createUuidv4(),
|
||||
name: options.name || "Test Subgraph",
|
||||
|
||||
// IO Nodes (required for subgraph functionality)
|
||||
inputNode: {
|
||||
id: -10, // SUBGRAPH_INPUT_ID
|
||||
bounding: [10, 100, 150, 126], // [x, y, width, height]
|
||||
pinned: false,
|
||||
},
|
||||
outputNode: {
|
||||
id: -20, // SUBGRAPH_OUTPUT_ID
|
||||
bounding: [400, 100, 140, 126], // [x, y, width, height]
|
||||
pinned: false,
|
||||
},
|
||||
|
||||
// IO definitions - will be populated by addInput/addOutput calls
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [],
|
||||
}
|
||||
|
||||
// Create the subgraph
|
||||
const subgraph = new Subgraph(rootGraph, subgraphData)
|
||||
|
||||
// Add requested inputs
|
||||
if (options.inputs) {
|
||||
for (const input of options.inputs) {
|
||||
subgraph.addInput(input.name, input.type)
|
||||
}
|
||||
} else if (options.inputCount) {
|
||||
for (let i = 0; i < options.inputCount; i++) {
|
||||
subgraph.addInput(`input_${i}`, "*")
|
||||
}
|
||||
}
|
||||
|
||||
// Add requested outputs
|
||||
if (options.outputs) {
|
||||
for (const output of options.outputs) {
|
||||
subgraph.addOutput(output.name, output.type)
|
||||
}
|
||||
} else if (options.outputCount) {
|
||||
for (let i = 0; i < options.outputCount; i++) {
|
||||
subgraph.addOutput(`output_${i}`, "*")
|
||||
}
|
||||
}
|
||||
|
||||
// Add test nodes if requested
|
||||
if (options.nodeCount) {
|
||||
for (let i = 0; i < options.nodeCount; i++) {
|
||||
const node = new LGraphNode(`Test Node ${i}`)
|
||||
node.addInput("in", "*")
|
||||
node.addOutput("out", "*")
|
||||
subgraph.add(node)
|
||||
}
|
||||
}
|
||||
|
||||
return subgraph
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SubgraphNode instance from a subgraph definition.
|
||||
* @param subgraph The subgraph definition to instantiate
|
||||
* @param options Configuration options for the node instance
|
||||
* @returns A SubgraphNode instance
|
||||
* @example
|
||||
* ```typescript
|
||||
* const subgraph = createTestSubgraph()
|
||||
* const subgraphNode = createTestSubgraphNode(subgraph, {
|
||||
* pos: [100, 200]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createTestSubgraphNode(
|
||||
subgraph: Subgraph,
|
||||
options: TestSubgraphNodeOptions = {},
|
||||
): SubgraphNode {
|
||||
const parentGraph = new LGraph()
|
||||
|
||||
const instanceData: ExportedSubgraphInstance = {
|
||||
id: options.id || 1,
|
||||
type: subgraph.id,
|
||||
pos: options.pos || [100, 100],
|
||||
size: options.size || [200, 100],
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
properties: {},
|
||||
flags: {},
|
||||
mode: 0,
|
||||
}
|
||||
|
||||
return new SubgraphNode(parentGraph, subgraph, instanceData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a nested hierarchy of subgraphs for testing deep nesting scenarios.
|
||||
* @param options Configuration for the nested structure
|
||||
* @returns Object containing the root graph and all created subgraphs
|
||||
* @example
|
||||
* ```typescript
|
||||
* const nested = createNestedSubgraphs({ depth: 3, nodesPerLevel: 2 })
|
||||
* // Creates: Root -> Subgraph1 -> Subgraph2 -> Subgraph3
|
||||
* ```
|
||||
*/
|
||||
export function createNestedSubgraphs(options: NestedSubgraphOptions = {}) {
|
||||
const {
|
||||
depth = 2,
|
||||
nodesPerLevel = 2,
|
||||
inputsPerSubgraph = 1,
|
||||
outputsPerSubgraph = 1,
|
||||
} = options
|
||||
|
||||
const rootGraph = new LGraph()
|
||||
const subgraphs: Subgraph[] = []
|
||||
const subgraphNodes: SubgraphNode[] = []
|
||||
|
||||
let currentParent = rootGraph
|
||||
|
||||
for (let level = 0; level < depth; level++) {
|
||||
// Create subgraph for this level
|
||||
const subgraph = createTestSubgraph({
|
||||
name: `Level ${level} Subgraph`,
|
||||
nodeCount: nodesPerLevel,
|
||||
inputCount: inputsPerSubgraph,
|
||||
outputCount: outputsPerSubgraph,
|
||||
})
|
||||
|
||||
subgraphs.push(subgraph)
|
||||
|
||||
// Create instance in parent
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, {
|
||||
pos: [100 + level * 200, 100],
|
||||
})
|
||||
|
||||
if (currentParent instanceof LGraph) {
|
||||
currentParent.add(subgraphNode)
|
||||
} else {
|
||||
currentParent.add(subgraphNode)
|
||||
}
|
||||
|
||||
subgraphNodes.push(subgraphNode)
|
||||
|
||||
// Next level will be nested inside this subgraph
|
||||
currentParent = subgraph
|
||||
}
|
||||
|
||||
return {
|
||||
rootGraph,
|
||||
subgraphs,
|
||||
subgraphNodes,
|
||||
depth,
|
||||
leafSubgraph: subgraphs.at(-1),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a subgraph has the expected structure.
|
||||
* This provides consistent validation across all tests.
|
||||
* @param subgraph The subgraph to validate
|
||||
* @param expected The expected structure
|
||||
* @example
|
||||
* ```typescript
|
||||
* assertSubgraphStructure(subgraph, {
|
||||
* inputCount: 2,
|
||||
* outputCount: 1,
|
||||
* name: "Expected Name"
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function assertSubgraphStructure(
|
||||
subgraph: Subgraph,
|
||||
expected: SubgraphStructureExpectation,
|
||||
): void {
|
||||
if (expected.inputCount !== undefined) {
|
||||
expect(subgraph.inputs.length).toBe(expected.inputCount)
|
||||
}
|
||||
|
||||
if (expected.outputCount !== undefined) {
|
||||
expect(subgraph.outputs.length).toBe(expected.outputCount)
|
||||
}
|
||||
|
||||
if (expected.nodeCount !== undefined) {
|
||||
expect(subgraph.nodes.length).toBe(expected.nodeCount)
|
||||
}
|
||||
|
||||
if (expected.name !== undefined) {
|
||||
expect(subgraph.name).toBe(expected.name)
|
||||
}
|
||||
|
||||
if (expected.hasInputNode !== false) {
|
||||
expect(subgraph.inputNode).toBeDefined()
|
||||
expect(subgraph.inputNode.id).toBe(-10)
|
||||
}
|
||||
|
||||
if (expected.hasOutputNode !== false) {
|
||||
expect(subgraph.outputNode).toBeDefined()
|
||||
expect(subgraph.outputNode.id).toBe(-20)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that events were fired in the expected sequence.
|
||||
* Useful for testing event-driven behavior.
|
||||
* @param capturedEvents Array of captured events
|
||||
* @param expectedSequence Expected sequence of event types
|
||||
* @example
|
||||
* ```typescript
|
||||
* verifyEventSequence(events, [
|
||||
* "adding-input",
|
||||
* "input-added",
|
||||
* "adding-output",
|
||||
* "output-added"
|
||||
* ])
|
||||
* ```
|
||||
*/
|
||||
export function verifyEventSequence<T = unknown>(
|
||||
capturedEvents: CapturedEvent<T>[],
|
||||
expectedSequence: string[],
|
||||
): void {
|
||||
expect(capturedEvents.length).toBe(expectedSequence.length)
|
||||
|
||||
for (const [i, element] of expectedSequence.entries()) {
|
||||
expect(capturedEvents[i].type).toBe(element)
|
||||
}
|
||||
|
||||
// Verify timestamps are in order
|
||||
for (let i = 1; i < capturedEvents.length; i++) {
|
||||
expect(capturedEvents[i].timestamp).toBeGreaterThanOrEqual(
|
||||
capturedEvents[i - 1].timestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates test subgraph data with optional overrides.
|
||||
* Useful for serialization/deserialization tests.
|
||||
* @param overrides Properties to override in the default data
|
||||
* @returns ExportedSubgraph data structure
|
||||
*/
|
||||
export function createTestSubgraphData(overrides: Partial<ExportedSubgraph> = {}): ExportedSubgraph {
|
||||
return {
|
||||
version: 1,
|
||||
nodes: [],
|
||||
links: {},
|
||||
groups: [],
|
||||
config: {},
|
||||
definitions: { subgraphs: [] },
|
||||
|
||||
id: createUuidv4(),
|
||||
name: "Test Data Subgraph",
|
||||
|
||||
inputNode: {
|
||||
id: -10,
|
||||
bounding: [10, 100, 150, 126],
|
||||
pinned: false,
|
||||
},
|
||||
outputNode: {
|
||||
id: -20,
|
||||
bounding: [400, 100, 140, 126],
|
||||
pinned: false,
|
||||
},
|
||||
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [],
|
||||
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complex subgraph with multiple nodes and connections.
|
||||
* Useful for testing realistic scenarios.
|
||||
* @param nodeCount Number of internal nodes to create
|
||||
* @returns Complex subgraph data structure
|
||||
*/
|
||||
export function createComplexSubgraphData(nodeCount: number = 5): ExportedSubgraph {
|
||||
const nodes = []
|
||||
const links: Record<string, {
|
||||
id: number
|
||||
origin_id: number
|
||||
origin_slot: number
|
||||
target_id: number
|
||||
target_slot: number
|
||||
type: string
|
||||
}> = {}
|
||||
|
||||
// Create internal nodes
|
||||
for (let i = 0; i < nodeCount; i++) {
|
||||
nodes.push({
|
||||
id: i + 1, // Start from 1 to avoid conflicts with IO nodes
|
||||
type: "basic/test",
|
||||
pos: [100 + i * 150, 200],
|
||||
size: [120, 60],
|
||||
inputs: [{ name: "in", type: "*", link: null }],
|
||||
outputs: [{ name: "out", type: "*", links: [] }],
|
||||
properties: { value: i },
|
||||
flags: {},
|
||||
mode: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// Create some internal links
|
||||
for (let i = 0; i < nodeCount - 1; i++) {
|
||||
const linkId = i + 1
|
||||
links[linkId] = {
|
||||
id: linkId,
|
||||
origin_id: i + 1,
|
||||
origin_slot: 0,
|
||||
target_id: i + 2,
|
||||
target_slot: 0,
|
||||
type: "*",
|
||||
}
|
||||
}
|
||||
|
||||
return createTestSubgraphData({
|
||||
nodes,
|
||||
links,
|
||||
inputs: [
|
||||
{ name: "input1", type: "number", pos: [0, 0] },
|
||||
{ name: "input2", type: "string", pos: [0, 1] },
|
||||
],
|
||||
outputs: [
|
||||
{ name: "output1", type: "number", pos: [0, 0] },
|
||||
{ name: "output2", type: "string", pos: [0, 1] },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event capture system for testing event sequences.
|
||||
* @param eventTarget The event target to monitor
|
||||
* @param eventTypes Array of event types to capture
|
||||
* @returns Object with captured events and helper methods
|
||||
*/
|
||||
export function createEventCapture<T = unknown>(
|
||||
eventTarget: EventTarget,
|
||||
eventTypes: string[],
|
||||
) {
|
||||
const capturedEvents: CapturedEvent<T>[] = []
|
||||
const listeners: Array<() => void> = []
|
||||
|
||||
// Set up listeners for each event type
|
||||
for (const eventType of eventTypes) {
|
||||
const listener = (event: Event) => {
|
||||
capturedEvents.push({
|
||||
type: eventType,
|
||||
detail: (event as CustomEvent<T>).detail,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(eventType, listener)
|
||||
listeners.push(() => eventTarget.removeEventListener(eventType, listener))
|
||||
}
|
||||
|
||||
return {
|
||||
events: capturedEvents,
|
||||
clear: () => { capturedEvents.length = 0 },
|
||||
cleanup: () => {
|
||||
// Remove all event listeners to prevent memory leaks
|
||||
for (const cleanup of listeners) cleanup()
|
||||
},
|
||||
getEventsByType: (type: string) => capturedEvents.filter(e => e.type === type),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to log subgraph structure for debugging tests.
|
||||
* @param subgraph The subgraph to inspect
|
||||
* @param label Optional label for the log output
|
||||
*/
|
||||
export function logSubgraphStructure(subgraph: Subgraph, label: string = "Subgraph"): void {
|
||||
console.log(`\n=== ${label} Structure ===`)
|
||||
console.log(`Name: ${subgraph.name}`)
|
||||
console.log(`ID: ${subgraph.id}`)
|
||||
console.log(`Inputs: ${subgraph.inputs.length}`)
|
||||
console.log(`Outputs: ${subgraph.outputs.length}`)
|
||||
console.log(`Nodes: ${subgraph.nodes.length}`)
|
||||
console.log(`Links: ${subgraph.links.size}`)
|
||||
|
||||
if (subgraph.inputs.length > 0) {
|
||||
console.log("Input details:", subgraph.inputs.map(i => ({ name: i.name, type: i.type })))
|
||||
}
|
||||
|
||||
if (subgraph.outputs.length > 0) {
|
||||
console.log("Output details:", subgraph.outputs.map(o => ({ name: o.name, type: o.type })))
|
||||
}
|
||||
|
||||
console.log("========================\n")
|
||||
}
|
||||
|
||||
// Re-export expect from vitest for convenience
|
||||
export { expect } from "vitest"
|
||||
444
test/subgraph/fixtures/testSubgraphs.json
Normal file
444
test/subgraph/fixtures/testSubgraphs.json
Normal file
@@ -0,0 +1,444 @@
|
||||
{
|
||||
"simpleSubgraph": {
|
||||
"version": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "basic/math",
|
||||
"pos": [200, 150],
|
||||
"size": [120, 60],
|
||||
"inputs": [
|
||||
{ "name": "a", "type": "number", "link": null },
|
||||
{ "name": "b", "type": "number", "link": null }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "result", "type": "number", "links": [] }
|
||||
],
|
||||
"properties": { "operation": "add" },
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
}
|
||||
],
|
||||
"links": {},
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"definitions": { "subgraphs": [] },
|
||||
|
||||
"id": "simple-subgraph-uuid",
|
||||
"name": "Simple Math Subgraph",
|
||||
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"type": "subgraph/input",
|
||||
"pos": [10, 100],
|
||||
"size": [140, 26],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"type": "subgraph/output",
|
||||
"pos": [400, 100],
|
||||
"size": [140, 26],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input_a",
|
||||
"type": "number",
|
||||
"pos": [0, 0]
|
||||
},
|
||||
{
|
||||
"name": "input_b",
|
||||
"type": "number",
|
||||
"pos": [0, 1]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "result",
|
||||
"type": "number",
|
||||
"pos": [0, 0]
|
||||
}
|
||||
],
|
||||
"widgets": []
|
||||
},
|
||||
|
||||
"complexSubgraph": {
|
||||
"version": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "math/multiply",
|
||||
"pos": [150, 100],
|
||||
"size": [120, 60],
|
||||
"inputs": [
|
||||
{ "name": "a", "type": "number", "link": null },
|
||||
{ "name": "b", "type": "number", "link": null }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "result", "type": "number", "links": [1] }
|
||||
],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "math/add",
|
||||
"pos": [300, 100],
|
||||
"size": [120, 60],
|
||||
"inputs": [
|
||||
{ "name": "a", "type": "number", "link": 1 },
|
||||
{ "name": "b", "type": "number", "link": null }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "result", "type": "number", "links": [2] }
|
||||
],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "logic/compare",
|
||||
"pos": [150, 200],
|
||||
"size": [120, 60],
|
||||
"inputs": [
|
||||
{ "name": "a", "type": "number", "link": null },
|
||||
{ "name": "b", "type": "number", "link": null }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "result", "type": "boolean", "links": [] }
|
||||
],
|
||||
"properties": { "operation": "greater_than" },
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "string/concat",
|
||||
"pos": [300, 200],
|
||||
"size": [120, 60],
|
||||
"inputs": [
|
||||
{ "name": "a", "type": "string", "link": null },
|
||||
{ "name": "b", "type": "string", "link": null }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "result", "type": "string", "links": [] }
|
||||
],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"1": {
|
||||
"id": 1,
|
||||
"origin_id": 1,
|
||||
"origin_slot": 0,
|
||||
"target_id": 2,
|
||||
"target_slot": 0,
|
||||
"type": "number"
|
||||
},
|
||||
"2": {
|
||||
"id": 2,
|
||||
"origin_id": 2,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"definitions": { "subgraphs": [] },
|
||||
|
||||
"id": "complex-subgraph-uuid",
|
||||
"name": "Complex Processing Subgraph",
|
||||
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"type": "subgraph/input",
|
||||
"pos": [10, 150],
|
||||
"size": [140, 86],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"type": "subgraph/output",
|
||||
"pos": [450, 150],
|
||||
"size": [140, 66],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
|
||||
"inputs": [
|
||||
{
|
||||
"name": "number1",
|
||||
"type": "number",
|
||||
"pos": [0, 0]
|
||||
},
|
||||
{
|
||||
"name": "number2",
|
||||
"type": "number",
|
||||
"pos": [0, 1]
|
||||
},
|
||||
{
|
||||
"name": "text1",
|
||||
"type": "string",
|
||||
"pos": [0, 2]
|
||||
},
|
||||
{
|
||||
"name": "text2",
|
||||
"type": "string",
|
||||
"pos": [0, 3]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "calculated_result",
|
||||
"type": "number",
|
||||
"pos": [0, 0]
|
||||
},
|
||||
{
|
||||
"name": "comparison_result",
|
||||
"type": "boolean",
|
||||
"pos": [0, 1]
|
||||
},
|
||||
{
|
||||
"name": "concatenated_text",
|
||||
"type": "string",
|
||||
"pos": [0, 2]
|
||||
}
|
||||
],
|
||||
"widgets": []
|
||||
},
|
||||
|
||||
"nestedSubgraphLevel1": {
|
||||
"version": 1,
|
||||
"nodes": [],
|
||||
"links": {},
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"version": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "basic/constant",
|
||||
"pos": [200, 100],
|
||||
"size": [100, 40],
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{ "name": "value", "type": "number", "links": [] }
|
||||
],
|
||||
"properties": { "value": 42 },
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
}
|
||||
],
|
||||
"links": {},
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"definitions": { "subgraphs": [] },
|
||||
|
||||
"id": "nested-level2-uuid",
|
||||
"name": "Level 2 Subgraph",
|
||||
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"type": "subgraph/input",
|
||||
"pos": [10, 100],
|
||||
"size": [140, 26],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"type": "subgraph/output",
|
||||
"pos": [350, 100],
|
||||
"size": [140, 26],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "constant_value",
|
||||
"type": "number",
|
||||
"pos": [0, 0]
|
||||
}
|
||||
],
|
||||
"widgets": []
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"id": "nested-level1-uuid",
|
||||
"name": "Level 1 Subgraph",
|
||||
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"type": "subgraph/input",
|
||||
"pos": [10, 100],
|
||||
"size": [140, 26],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"type": "subgraph/output",
|
||||
"pos": [400, 100],
|
||||
"size": [140, 26],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
|
||||
"inputs": [
|
||||
{
|
||||
"name": "external_input",
|
||||
"type": "string",
|
||||
"pos": [0, 0]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "processed_output",
|
||||
"type": "number",
|
||||
"pos": [0, 0]
|
||||
}
|
||||
],
|
||||
"widgets": []
|
||||
},
|
||||
|
||||
"emptySubgraph": {
|
||||
"version": 1,
|
||||
"nodes": [],
|
||||
"links": {},
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"definitions": { "subgraphs": [] },
|
||||
|
||||
"id": "empty-subgraph-uuid",
|
||||
"name": "Empty Subgraph",
|
||||
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"type": "subgraph/input",
|
||||
"pos": [10, 100],
|
||||
"size": [140, 26],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"type": "subgraph/output",
|
||||
"pos": [400, 100],
|
||||
"size": [140, 26],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"widgets": []
|
||||
},
|
||||
|
||||
"maxIOSubgraph": {
|
||||
"version": 1,
|
||||
"nodes": [],
|
||||
"links": {},
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"definitions": { "subgraphs": [] },
|
||||
|
||||
"id": "max-io-subgraph-uuid",
|
||||
"name": "Max I/O Subgraph",
|
||||
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"type": "subgraph/input",
|
||||
"pos": [10, 100],
|
||||
"size": [140, 200],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"type": "subgraph/output",
|
||||
"pos": [400, 100],
|
||||
"size": [140, 200],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"flags": {},
|
||||
"mode": 0
|
||||
},
|
||||
|
||||
"inputs": [
|
||||
{ "name": "input_0", "type": "number", "pos": [0, 0] },
|
||||
{ "name": "input_1", "type": "string", "pos": [0, 1] },
|
||||
{ "name": "input_2", "type": "boolean", "pos": [0, 2] },
|
||||
{ "name": "input_3", "type": "number", "pos": [0, 3] },
|
||||
{ "name": "input_4", "type": "string", "pos": [0, 4] },
|
||||
{ "name": "input_5", "type": "boolean", "pos": [0, 5] },
|
||||
{ "name": "input_6", "type": "number", "pos": [0, 6] },
|
||||
{ "name": "input_7", "type": "string", "pos": [0, 7] },
|
||||
{ "name": "input_8", "type": "boolean", "pos": [0, 8] },
|
||||
{ "name": "input_9", "type": "number", "pos": [0, 9] }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "output_0", "type": "number", "pos": [0, 0] },
|
||||
{ "name": "output_1", "type": "string", "pos": [0, 1] },
|
||||
{ "name": "output_2", "type": "boolean", "pos": [0, 2] },
|
||||
{ "name": "output_3", "type": "number", "pos": [0, 3] },
|
||||
{ "name": "output_4", "type": "string", "pos": [0, 4] },
|
||||
{ "name": "output_5", "type": "boolean", "pos": [0, 5] },
|
||||
{ "name": "output_6", "type": "number", "pos": [0, 6] },
|
||||
{ "name": "output_7", "type": "string", "pos": [0, 7] },
|
||||
{ "name": "output_8", "type": "boolean", "pos": [0, 8] },
|
||||
{ "name": "output_9", "type": "number", "pos": [0, 9] }
|
||||
],
|
||||
"widgets": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user