mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
npm run format
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraph, LGraphNode } from "@/lib/litegraph/src/litegraph"
|
||||
import { ExecutableNodeDTO } from "@/lib/litegraph/src/subgraph/ExecutableNodeDTO"
|
||||
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { ExecutableNodeDTO } from '@/lib/litegraph/src/subgraph/ExecutableNodeDTO'
|
||||
|
||||
import {
|
||||
createNestedSubgraphs,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
} from "./fixtures/subgraphHelpers"
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("ExecutableNodeDTO Creation", () => {
|
||||
it("should create DTO from regular node", () => {
|
||||
describe('ExecutableNodeDTO Creation', () => {
|
||||
it('should create DTO from regular node', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Test Node")
|
||||
node.addInput("in", "number")
|
||||
node.addOutput("out", "string")
|
||||
const node = new LGraphNode('Test Node')
|
||||
node.addInput('in', 'number')
|
||||
node.addOutput('out', 'string')
|
||||
graph.add(node)
|
||||
|
||||
const executableNodes = new Map()
|
||||
@@ -26,44 +26,44 @@ describe("ExecutableNodeDTO Creation", () => {
|
||||
expect(dto.id).toBe(node.id.toString())
|
||||
})
|
||||
|
||||
it("should create DTO with subgraph path", () => {
|
||||
it('should create DTO with subgraph path', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Inner Node")
|
||||
const node = new LGraphNode('Inner Node')
|
||||
node.id = 42
|
||||
graph.add(node)
|
||||
const subgraphPath = ["10", "20"] as const
|
||||
const subgraphPath = ['10', '20'] as const
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, subgraphPath, new Map(), undefined)
|
||||
|
||||
expect(dto.subgraphNodePath).toBe(subgraphPath)
|
||||
expect(dto.id).toBe("10:20:42")
|
||||
expect(dto.id).toBe('10:20:42')
|
||||
})
|
||||
|
||||
it("should clone input slot data", () => {
|
||||
it('should clone input slot data', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Test Node")
|
||||
node.addInput("input1", "number")
|
||||
node.addInput("input2", "string")
|
||||
const node = new LGraphNode('Test Node')
|
||||
node.addInput('input1', 'number')
|
||||
node.addInput('input2', 'string')
|
||||
node.inputs[0].link = 123 // Simulate connected input
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
|
||||
|
||||
expect(dto.inputs).toHaveLength(2)
|
||||
expect(dto.inputs[0].name).toBe("input1")
|
||||
expect(dto.inputs[0].type).toBe("number")
|
||||
expect(dto.inputs[0].name).toBe('input1')
|
||||
expect(dto.inputs[0].type).toBe('number')
|
||||
expect(dto.inputs[0].linkId).toBe(123)
|
||||
expect(dto.inputs[1].name).toBe("input2")
|
||||
expect(dto.inputs[1].type).toBe("string")
|
||||
expect(dto.inputs[1].name).toBe('input2')
|
||||
expect(dto.inputs[1].type).toBe('string')
|
||||
expect(dto.inputs[1].linkId).toBeNull()
|
||||
|
||||
// Should be a copy, not reference
|
||||
expect(dto.inputs).not.toBe(node.inputs)
|
||||
})
|
||||
|
||||
it("should inherit graph reference", () => {
|
||||
it('should inherit graph reference', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Test Node")
|
||||
const node = new LGraphNode('Test Node')
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
|
||||
@@ -71,9 +71,9 @@ describe("ExecutableNodeDTO Creation", () => {
|
||||
expect(dto.graph).toBe(graph)
|
||||
})
|
||||
|
||||
it("should wrap applyToGraph method if present", () => {
|
||||
it('should wrap applyToGraph method if present', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Test Node")
|
||||
const node = new LGraphNode('Test Node')
|
||||
const mockApplyToGraph = vi.fn()
|
||||
Object.assign(node, { applyToGraph: mockApplyToGraph })
|
||||
graph.add(node)
|
||||
@@ -83,7 +83,7 @@ describe("ExecutableNodeDTO Creation", () => {
|
||||
expect(dto.applyToGraph).toBeDefined()
|
||||
|
||||
// Test that wrapper calls original method
|
||||
const args = ["arg1", "arg2"]
|
||||
const args = ['arg1', 'arg2']
|
||||
dto.applyToGraph!(args[0], args[1])
|
||||
|
||||
expect(mockApplyToGraph).toHaveBeenCalledWith(args[0], args[1])
|
||||
@@ -91,7 +91,7 @@ describe("ExecutableNodeDTO Creation", () => {
|
||||
|
||||
it("should not create applyToGraph wrapper if method doesn't exist", () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Test Node")
|
||||
const node = new LGraphNode('Test Node')
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
|
||||
@@ -100,65 +100,65 @@ describe("ExecutableNodeDTO Creation", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("ExecutableNodeDTO Path-Based IDs", () => {
|
||||
it("should generate simple ID for root node", () => {
|
||||
describe('ExecutableNodeDTO Path-Based IDs', () => {
|
||||
it('should generate simple ID for root node', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Root Node")
|
||||
const node = new LGraphNode('Root Node')
|
||||
node.id = 5
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
|
||||
|
||||
expect(dto.id).toBe("5")
|
||||
expect(dto.id).toBe('5')
|
||||
})
|
||||
|
||||
it("should generate path-based ID for nested node", () => {
|
||||
it('should generate path-based ID for nested node', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Nested Node")
|
||||
const node = new LGraphNode('Nested Node')
|
||||
node.id = 3
|
||||
graph.add(node)
|
||||
const path = ["1", "2"] as const
|
||||
const path = ['1', '2'] as const
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, path, new Map(), undefined)
|
||||
|
||||
expect(dto.id).toBe("1:2:3")
|
||||
expect(dto.id).toBe('1:2:3')
|
||||
})
|
||||
|
||||
it("should handle deep nesting paths", () => {
|
||||
it('should handle deep nesting paths', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Deep Node")
|
||||
const node = new LGraphNode('Deep Node')
|
||||
node.id = 99
|
||||
graph.add(node)
|
||||
const path = ["1", "2", "3", "4", "5"] as const
|
||||
const path = ['1', '2', '3', '4', '5'] as const
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, path, new Map(), undefined)
|
||||
|
||||
expect(dto.id).toBe("1:2:3:4:5:99")
|
||||
expect(dto.id).toBe('1:2:3:4:5:99')
|
||||
})
|
||||
|
||||
it("should handle string and number IDs consistently", () => {
|
||||
it('should handle string and number IDs consistently', () => {
|
||||
const graph = new LGraph()
|
||||
const node1 = new LGraphNode("Node 1")
|
||||
const node1 = new LGraphNode('Node 1')
|
||||
node1.id = 10
|
||||
graph.add(node1)
|
||||
|
||||
const node2 = new LGraphNode("Node 2")
|
||||
const node2 = new LGraphNode('Node 2')
|
||||
node2.id = 20
|
||||
graph.add(node2)
|
||||
|
||||
const dto1 = new ExecutableNodeDTO(node1, ["5"], new Map(), undefined)
|
||||
const dto2 = new ExecutableNodeDTO(node2, ["5"], new Map(), undefined)
|
||||
const dto1 = new ExecutableNodeDTO(node1, ['5'], new Map(), undefined)
|
||||
const dto2 = new ExecutableNodeDTO(node2, ['5'], new Map(), undefined)
|
||||
|
||||
expect(dto1.id).toBe("5:10")
|
||||
expect(dto2.id).toBe("5:20")
|
||||
expect(dto1.id).toBe('5:10')
|
||||
expect(dto2.id).toBe('5:20')
|
||||
})
|
||||
})
|
||||
|
||||
describe("ExecutableNodeDTO Input Resolution", () => {
|
||||
it("should return undefined for unconnected inputs", () => {
|
||||
describe('ExecutableNodeDTO Input Resolution', () => {
|
||||
it('should return undefined for unconnected inputs', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Test Node")
|
||||
node.addInput("in", "number")
|
||||
const node = new LGraphNode('Test Node')
|
||||
node.addInput('in', 'number')
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
|
||||
@@ -168,27 +168,27 @@ describe("ExecutableNodeDTO Input Resolution", () => {
|
||||
expect(resolved).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should throw for non-existent input slots", () => {
|
||||
it('should throw for non-existent input slots', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("No Input Node")
|
||||
const node = new LGraphNode('No Input Node')
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
|
||||
|
||||
// Should throw SlotIndexError for non-existent input
|
||||
expect(() => dto.resolveInput(0)).toThrow("No input found for flattened id")
|
||||
expect(() => dto.resolveInput(0)).toThrow('No input found for flattened id')
|
||||
})
|
||||
|
||||
it("should handle subgraph boundary inputs", () => {
|
||||
it('should handle subgraph boundary inputs', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input1", type: "number" }],
|
||||
nodeCount: 1,
|
||||
inputs: [{ name: 'input1', type: 'number' }],
|
||||
nodeCount: 1
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Get the inner node and create DTO
|
||||
const innerNode = subgraph.nodes[0]
|
||||
const dto = new ExecutableNodeDTO(innerNode, ["1"], new Map(), subgraphNode)
|
||||
const dto = new ExecutableNodeDTO(innerNode, ['1'], new Map(), subgraphNode)
|
||||
|
||||
// Should return undefined for unconnected input
|
||||
const resolved = dto.resolveInput(0)
|
||||
@@ -196,17 +196,17 @@ describe("ExecutableNodeDTO Input Resolution", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("ExecutableNodeDTO Output Resolution", () => {
|
||||
it("should resolve outputs for simple nodes", () => {
|
||||
describe('ExecutableNodeDTO Output Resolution', () => {
|
||||
it('should resolve outputs for simple nodes', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Test Node")
|
||||
node.addOutput("out", "string")
|
||||
const node = new LGraphNode('Test Node')
|
||||
node.addOutput('out', 'string')
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
|
||||
|
||||
// resolveOutput requires type and visited parameters
|
||||
const resolved = dto.resolveOutput(0, "string", new Set())
|
||||
const resolved = dto.resolveOutput(0, 'string', new Set())
|
||||
|
||||
expect(resolved).toBeDefined()
|
||||
expect(resolved?.node).toBe(dto)
|
||||
@@ -214,60 +214,60 @@ describe("ExecutableNodeDTO Output Resolution", () => {
|
||||
expect(resolved?.origin_slot).toBe(0)
|
||||
})
|
||||
|
||||
it("should resolve cross-boundary outputs in subgraphs", () => {
|
||||
it('should resolve cross-boundary outputs in subgraphs', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
outputs: [{ name: "output1", type: "string" }],
|
||||
nodeCount: 1,
|
||||
outputs: [{ name: 'output1', type: 'string' }],
|
||||
nodeCount: 1
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Get the inner node and create DTO
|
||||
const innerNode = subgraph.nodes[0]
|
||||
const dto = new ExecutableNodeDTO(innerNode, ["1"], new Map(), subgraphNode)
|
||||
const dto = new ExecutableNodeDTO(innerNode, ['1'], new Map(), subgraphNode)
|
||||
|
||||
const resolved = dto.resolveOutput(0, "string", new Set())
|
||||
const resolved = dto.resolveOutput(0, 'string', new Set())
|
||||
|
||||
expect(resolved).toBeDefined()
|
||||
})
|
||||
|
||||
it("should handle nodes with no outputs", () => {
|
||||
it('should handle nodes with no outputs', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("No Output Node")
|
||||
const node = new LGraphNode('No Output Node')
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
|
||||
|
||||
// For regular nodes, resolveOutput returns the node itself even if no outputs
|
||||
// This tests the current implementation behavior
|
||||
const resolved = dto.resolveOutput(0, "string", new Set())
|
||||
const resolved = dto.resolveOutput(0, 'string', new Set())
|
||||
expect(resolved).toBeDefined()
|
||||
expect(resolved?.node).toBe(dto)
|
||||
expect(resolved?.origin_slot).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ExecutableNodeDTO Properties", () => {
|
||||
it("should provide access to basic properties", () => {
|
||||
describe('ExecutableNodeDTO Properties', () => {
|
||||
it('should provide access to basic properties', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Test Node")
|
||||
const node = new LGraphNode('Test Node')
|
||||
node.id = 42
|
||||
node.addInput("input", "number")
|
||||
node.addOutput("output", "string")
|
||||
node.addInput('input', 'number')
|
||||
node.addOutput('output', 'string')
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, ["1", "2"], new Map(), undefined)
|
||||
const dto = new ExecutableNodeDTO(node, ['1', '2'], new Map(), undefined)
|
||||
|
||||
expect(dto.id).toBe("1:2:42")
|
||||
expect(dto.id).toBe('1:2:42')
|
||||
expect(dto.type).toBe(node.type)
|
||||
expect(dto.title).toBe(node.title)
|
||||
expect(dto.mode).toBe(node.mode)
|
||||
expect(dto.isVirtualNode).toBe(node.isVirtualNode)
|
||||
})
|
||||
|
||||
it("should provide access to input information", () => {
|
||||
it('should provide access to input information', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Test Node")
|
||||
node.addInput("testInput", "number")
|
||||
const node = new LGraphNode('Test Node')
|
||||
node.addInput('testInput', 'number')
|
||||
node.inputs[0].link = 999 // Simulate connection
|
||||
graph.add(node)
|
||||
|
||||
@@ -275,35 +275,35 @@ describe("ExecutableNodeDTO Properties", () => {
|
||||
|
||||
expect(dto.inputs).toBeDefined()
|
||||
expect(dto.inputs).toHaveLength(1)
|
||||
expect(dto.inputs[0].name).toBe("testInput")
|
||||
expect(dto.inputs[0].type).toBe("number")
|
||||
expect(dto.inputs[0].name).toBe('testInput')
|
||||
expect(dto.inputs[0].type).toBe('number')
|
||||
expect(dto.inputs[0].linkId).toBe(999)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ExecutableNodeDTO Memory Efficiency", () => {
|
||||
it("should create lightweight objects", () => {
|
||||
describe('ExecutableNodeDTO Memory Efficiency', () => {
|
||||
it('should create lightweight objects', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Test Node")
|
||||
node.addInput("in1", "number")
|
||||
node.addInput("in2", "string")
|
||||
node.addOutput("out1", "number")
|
||||
node.addOutput("out2", "string")
|
||||
const node = new LGraphNode('Test Node')
|
||||
node.addInput('in1', 'number')
|
||||
node.addInput('in2', 'string')
|
||||
node.addOutput('out1', 'number')
|
||||
node.addOutput('out2', 'string')
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, ["1"], new Map(), undefined)
|
||||
const dto = new ExecutableNodeDTO(node, ['1'], new Map(), undefined)
|
||||
|
||||
// DTO should be lightweight - only essential properties
|
||||
expect(dto.node).toBe(node) // Reference, not copy
|
||||
expect(dto.subgraphNodePath).toEqual(["1"]) // Reference to path
|
||||
expect(dto.subgraphNodePath).toEqual(['1']) // Reference to path
|
||||
expect(dto.inputs).toHaveLength(2) // Copied input data only
|
||||
|
||||
// Should not duplicate heavy node data
|
||||
expect(dto.hasOwnProperty("outputs")).toBe(false) // Outputs not copied
|
||||
expect(dto.hasOwnProperty("widgets")).toBe(false) // Widgets not copied
|
||||
expect(dto.hasOwnProperty('outputs')).toBe(false) // Outputs not copied
|
||||
expect(dto.hasOwnProperty('widgets')).toBe(false) // Widgets not copied
|
||||
})
|
||||
|
||||
it("should handle disposal without memory leaks", () => {
|
||||
it('should handle disposal without memory leaks', () => {
|
||||
const graph = new LGraph()
|
||||
const nodes: ExecutableNodeDTO[] = []
|
||||
|
||||
@@ -312,7 +312,7 @@ describe("ExecutableNodeDTO Memory Efficiency", () => {
|
||||
const node = new LGraphNode(`Node ${i}`)
|
||||
node.id = i
|
||||
graph.add(node)
|
||||
const dto = new ExecutableNodeDTO(node, ["parent"], new Map(), undefined)
|
||||
const dto = new ExecutableNodeDTO(node, ['parent'], new Map(), undefined)
|
||||
nodes.push(dto)
|
||||
}
|
||||
|
||||
@@ -326,12 +326,12 @@ describe("ExecutableNodeDTO Memory Efficiency", () => {
|
||||
expect(nodes).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not retain unnecessary references", () => {
|
||||
it('should not retain unnecessary references', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 1 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const innerNode = subgraph.nodes[0]
|
||||
|
||||
const dto = new ExecutableNodeDTO(innerNode, ["1"], new Map(), subgraphNode)
|
||||
const dto = new ExecutableNodeDTO(innerNode, ['1'], new Map(), subgraphNode)
|
||||
|
||||
// Should hold necessary references
|
||||
expect(dto.node).toBe(innerNode)
|
||||
@@ -339,13 +339,13 @@ describe("ExecutableNodeDTO Memory Efficiency", () => {
|
||||
expect(dto.graph).toBe(innerNode.graph)
|
||||
|
||||
// Should not hold heavy references that prevent GC
|
||||
expect(dto.hasOwnProperty("parentGraph")).toBe(false)
|
||||
expect(dto.hasOwnProperty("rootGraph")).toBe(false)
|
||||
expect(dto.hasOwnProperty('parentGraph')).toBe(false)
|
||||
expect(dto.hasOwnProperty('rootGraph')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ExecutableNodeDTO Integration", () => {
|
||||
it("should work with SubgraphNode flattening", () => {
|
||||
describe('ExecutableNodeDTO Integration', () => {
|
||||
it('should work with SubgraphNode flattening', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 3 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -356,13 +356,13 @@ describe("ExecutableNodeDTO Integration", () => {
|
||||
expect(flattened[0].id).toMatch(/^1:\d+$/)
|
||||
})
|
||||
|
||||
it.skip("should handle nested subgraph flattening", () => {
|
||||
it.skip('should handle nested subgraph flattening', () => {
|
||||
// FIXME: Complex nested structure requires proper parent graph setup
|
||||
// This test needs investigation of how resolveSubgraphIdPath works
|
||||
// Skip for now - will implement in edge cases test file
|
||||
const nested = createNestedSubgraphs({
|
||||
depth: 2,
|
||||
nodesPerLevel: 1,
|
||||
nodesPerLevel: 1
|
||||
})
|
||||
|
||||
const rootSubgraphNode = nested.subgraphNodes[0]
|
||||
@@ -370,19 +370,24 @@ describe("ExecutableNodeDTO Integration", () => {
|
||||
const flattened = rootSubgraphNode.getInnerNodes(executableNodes)
|
||||
|
||||
expect(flattened.length).toBeGreaterThan(0)
|
||||
const hierarchicalIds = flattened.filter(dto => dto.id.includes(":"))
|
||||
const hierarchicalIds = flattened.filter((dto) => dto.id.includes(':'))
|
||||
expect(hierarchicalIds.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should preserve original node properties through DTO", () => {
|
||||
it('should preserve original node properties through DTO', () => {
|
||||
const graph = new LGraph()
|
||||
const originalNode = new LGraphNode("Original")
|
||||
const originalNode = new LGraphNode('Original')
|
||||
originalNode.id = 123
|
||||
originalNode.addInput("test", "number")
|
||||
originalNode.addInput('test', 'number')
|
||||
originalNode.properties = { value: 42 }
|
||||
graph.add(originalNode)
|
||||
|
||||
const dto = new ExecutableNodeDTO(originalNode, ["parent"], new Map(), undefined)
|
||||
const dto = new ExecutableNodeDTO(
|
||||
originalNode,
|
||||
['parent'],
|
||||
new Map(),
|
||||
undefined
|
||||
)
|
||||
|
||||
// DTO should provide access to original node properties
|
||||
expect(dto.node.id).toBe(123)
|
||||
@@ -390,26 +395,31 @@ describe("ExecutableNodeDTO Integration", () => {
|
||||
expect(dto.node.properties.value).toBe(42)
|
||||
|
||||
// But DTO ID should be path-based
|
||||
expect(dto.id).toBe("parent:123")
|
||||
expect(dto.id).toBe('parent:123')
|
||||
})
|
||||
|
||||
it("should handle execution context correctly", () => {
|
||||
it('should handle execution context correctly', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 1 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 99 })
|
||||
const innerNode = subgraph.nodes[0]
|
||||
innerNode.id = 55
|
||||
|
||||
const dto = new ExecutableNodeDTO(innerNode, ["99"], new Map(), subgraphNode)
|
||||
const dto = new ExecutableNodeDTO(
|
||||
innerNode,
|
||||
['99'],
|
||||
new Map(),
|
||||
subgraphNode
|
||||
)
|
||||
|
||||
// DTO provides execution context
|
||||
expect(dto.id).toBe("99:55") // Path-based execution ID
|
||||
expect(dto.id).toBe('99:55') // Path-based execution ID
|
||||
expect(dto.node.id).toBe(55) // Original node ID preserved
|
||||
expect(dto.subgraphNode?.id).toBe(99) // Subgraph context
|
||||
})
|
||||
})
|
||||
|
||||
describe("ExecutableNodeDTO Scale Testing", () => {
|
||||
it("should create DTOs at scale", () => {
|
||||
describe('ExecutableNodeDTO Scale Testing', () => {
|
||||
it('should create DTOs at scale', () => {
|
||||
const graph = new LGraph()
|
||||
const startTime = performance.now()
|
||||
const dtos: ExecutableNodeDTO[] = []
|
||||
@@ -418,10 +428,10 @@ describe("ExecutableNodeDTO Scale Testing", () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const node = new LGraphNode(`Node ${i}`)
|
||||
node.id = i
|
||||
node.addInput("in", "number")
|
||||
node.addInput('in', 'number')
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, ["parent"], new Map(), undefined)
|
||||
const dto = new ExecutableNodeDTO(node, ['parent'], new Map(), undefined)
|
||||
dtos.push(dto)
|
||||
}
|
||||
|
||||
@@ -430,29 +440,31 @@ describe("ExecutableNodeDTO Scale Testing", () => {
|
||||
|
||||
expect(dtos).toHaveLength(1000)
|
||||
// Test deterministic properties instead of flaky timing
|
||||
expect(dtos[0].id).toBe("parent:0")
|
||||
expect(dtos[999].id).toBe("parent:999")
|
||||
expect(dtos[0].id).toBe('parent:0')
|
||||
expect(dtos[999].id).toBe('parent:999')
|
||||
expect(dtos.every((dto, i) => dto.id === `parent:${i}`)).toBe(true)
|
||||
|
||||
console.log(`Created 1000 DTOs in ${duration.toFixed(2)}ms`)
|
||||
})
|
||||
|
||||
it("should handle complex path generation correctly", () => {
|
||||
it('should handle complex path generation correctly', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode("Deep Node")
|
||||
const node = new LGraphNode('Deep Node')
|
||||
node.id = 999
|
||||
graph.add(node)
|
||||
|
||||
// Test deterministic path generation behavior
|
||||
const testCases = [
|
||||
{ depth: 1, expectedId: "1:999" },
|
||||
{ depth: 3, expectedId: "1:2:3:999" },
|
||||
{ depth: 5, expectedId: "1:2:3:4:5:999" },
|
||||
{ depth: 10, expectedId: "1:2:3:4:5:6:7:8:9:10:999" },
|
||||
{ depth: 1, expectedId: '1:999' },
|
||||
{ depth: 3, expectedId: '1:2:3:999' },
|
||||
{ depth: 5, expectedId: '1:2:3:4:5:999' },
|
||||
{ depth: 10, expectedId: '1:2:3:4:5:6:7:8:9:10:999' }
|
||||
]
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const path = Array.from({ length: testCase.depth }, (_, i) => (i + 1).toString())
|
||||
const path = Array.from({ length: testCase.depth }, (_, i) =>
|
||||
(i + 1).toString()
|
||||
)
|
||||
const dto = new ExecutableNodeDTO(node, path, new Map(), undefined)
|
||||
expect(dto.id).toBe(testCase.expectedId)
|
||||
}
|
||||
|
||||
@@ -5,61 +5,62 @@
|
||||
* patterns for the rest of the testing team. These tests cover construction,
|
||||
* basic I/O management, and known issues.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { RecursionError } from '@/lib/litegraph/src/infrastructure/RecursionError'
|
||||
import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { createUuidv4 } from '@/lib/litegraph/src/utils/uuid'
|
||||
|
||||
import { RecursionError } from "@/lib/litegraph/src/infrastructure/RecursionError"
|
||||
import { LGraph, Subgraph } from "@/lib/litegraph/src/litegraph"
|
||||
import { createUuidv4 } from "@/lib/litegraph/src/utils/uuid"
|
||||
|
||||
import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
import { subgraphTest } from './fixtures/subgraphFixtures'
|
||||
import {
|
||||
assertSubgraphStructure,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphData,
|
||||
} from "./fixtures/subgraphHelpers"
|
||||
createTestSubgraphData
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("Subgraph Construction", () => {
|
||||
it("should create a subgraph with minimal data", () => {
|
||||
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",
|
||||
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.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", () => {
|
||||
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")
|
||||
}).toThrow('Root graph is required')
|
||||
})
|
||||
|
||||
it("should accept custom name and ID", () => {
|
||||
it('should accept custom name and ID', () => {
|
||||
const customId = createUuidv4()
|
||||
const customName = "My Custom Subgraph"
|
||||
const customName = 'My Custom Subgraph'
|
||||
|
||||
const subgraph = createTestSubgraph({
|
||||
id: customId,
|
||||
name: customName,
|
||||
name: customName
|
||||
})
|
||||
|
||||
expect(subgraph.id).toBe(customId)
|
||||
expect(subgraph.name).toBe(customName)
|
||||
})
|
||||
|
||||
it("should initialize with empty inputs and outputs", () => {
|
||||
it('should initialize with empty inputs and outputs', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
expect(subgraph.inputs).toHaveLength(0)
|
||||
@@ -67,7 +68,7 @@ describe("Subgraph Construction", () => {
|
||||
expect(subgraph.widgets).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should have properly configured input and output nodes", () => {
|
||||
it('should have properly configured input and output nodes', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// Input node should be positioned on the left
|
||||
@@ -82,50 +83,56 @@ describe("Subgraph Construction", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Subgraph Input/Output Management", () => {
|
||||
subgraphTest("should add a single input", ({ emptySubgraph }) => {
|
||||
const input = emptySubgraph.addInput("test_input", "number")
|
||||
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(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")
|
||||
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(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")
|
||||
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)
|
||||
})
|
||||
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")
|
||||
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)
|
||||
})
|
||||
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 }) => {
|
||||
subgraphTest('should remove inputs correctly', ({ simpleSubgraph }) => {
|
||||
// Add a second input first
|
||||
simpleSubgraph.addInput("second_input", "string")
|
||||
simpleSubgraph.addInput('second_input', 'string')
|
||||
expect(simpleSubgraph.inputs).toHaveLength(2)
|
||||
|
||||
// Remove the first input
|
||||
@@ -133,14 +140,14 @@ describe("Subgraph Input/Output Management", () => {
|
||||
simpleSubgraph.removeInput(firstInput)
|
||||
|
||||
expect(simpleSubgraph.inputs).toHaveLength(1)
|
||||
expect(simpleSubgraph.inputs[0].name).toBe("second_input")
|
||||
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 }) => {
|
||||
subgraphTest('should remove outputs correctly', ({ simpleSubgraph }) => {
|
||||
// Add a second output first
|
||||
simpleSubgraph.addOutput("second_output", "string")
|
||||
simpleSubgraph.addOutput('second_output', 'string')
|
||||
expect(simpleSubgraph.outputs).toHaveLength(2)
|
||||
|
||||
// Remove the first output
|
||||
@@ -148,48 +155,54 @@ describe("Subgraph Input/Output Management", () => {
|
||||
simpleSubgraph.removeOutput(firstOutput)
|
||||
|
||||
expect(simpleSubgraph.outputs).toHaveLength(1)
|
||||
expect(simpleSubgraph.outputs[0].name).toBe("second_output")
|
||||
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 Serialization", () => {
|
||||
subgraphTest("should serialize empty subgraph", ({ emptySubgraph }) => {
|
||||
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.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")
|
||||
expect(typeof serialized.links).toBe('object')
|
||||
})
|
||||
|
||||
subgraphTest("should serialize subgraph with inputs and outputs", ({ simpleSubgraph }) => {
|
||||
const serialized = simpleSubgraph.asSerialisable()
|
||||
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")
|
||||
})
|
||||
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()
|
||||
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)
|
||||
})
|
||||
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.todo("should enforce MAX_NESTED_SUBGRAPHS limit", () => {
|
||||
describe('Subgraph Known Issues', () => {
|
||||
it.todo('should enforce MAX_NESTED_SUBGRAPHS limit', () => {
|
||||
// This test documents that MAX_NESTED_SUBGRAPHS = 1000 is defined
|
||||
// but not actually enforced anywhere in the code.
|
||||
//
|
||||
@@ -199,24 +212,24 @@ describe("Subgraph Known Issues", () => {
|
||||
// This safety limit should be implemented to prevent runaway recursion.
|
||||
})
|
||||
|
||||
it("should provide MAX_NESTED_SUBGRAPHS constant", () => {
|
||||
it('should provide MAX_NESTED_SUBGRAPHS constant', () => {
|
||||
expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBe(1000)
|
||||
})
|
||||
|
||||
it("should have recursion detection in place", () => {
|
||||
it('should have recursion detection in place', () => {
|
||||
// Verify that RecursionError is available and can be thrown
|
||||
expect(() => {
|
||||
throw new RecursionError("test recursion")
|
||||
throw new RecursionError('test recursion')
|
||||
}).toThrow(RecursionError)
|
||||
|
||||
expect(() => {
|
||||
throw new RecursionError("test recursion")
|
||||
}).toThrow("test recursion")
|
||||
throw new RecursionError('test recursion')
|
||||
}).toThrow('test recursion')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Subgraph Root Graph Relationship", () => {
|
||||
it("should maintain reference to root graph", () => {
|
||||
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)
|
||||
@@ -224,16 +237,16 @@ describe("Subgraph Root Graph Relationship", () => {
|
||||
expect(subgraph.rootGraph).toBe(rootGraph)
|
||||
})
|
||||
|
||||
it("should inherit root graph in nested subgraphs", () => {
|
||||
it('should inherit root graph in nested subgraphs', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const parentData = createTestSubgraphData({
|
||||
name: "Parent Subgraph",
|
||||
name: 'Parent Subgraph'
|
||||
})
|
||||
const parentSubgraph = new Subgraph(rootGraph, parentData)
|
||||
|
||||
// Create a nested subgraph
|
||||
const nestedData = createTestSubgraphData({
|
||||
name: "Nested Subgraph",
|
||||
name: 'Nested Subgraph'
|
||||
})
|
||||
const nestedSubgraph = new Subgraph(rootGraph, nestedData)
|
||||
|
||||
@@ -242,34 +255,40 @@ describe("Subgraph Root Graph Relationship", () => {
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
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")
|
||||
})
|
||||
// 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
|
||||
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")
|
||||
})
|
||||
// Now try to remove it again
|
||||
expect(() => {
|
||||
emptySubgraph.removeOutput(fakeOutput)
|
||||
}).toThrow('Output not found')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe("Subgraph Integration", () => {
|
||||
describe('Subgraph Integration', () => {
|
||||
it("should work with LGraph's node management", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
nodeCount: 3,
|
||||
nodeCount: 3
|
||||
})
|
||||
|
||||
// Verify nodes were added to the subgraph
|
||||
@@ -278,12 +297,12 @@ describe("Subgraph Integration", () => {
|
||||
// Verify we can access nodes by ID
|
||||
const firstNode = subgraph.getNodeById(1)
|
||||
expect(firstNode).toBeDefined()
|
||||
expect(firstNode?.title).toContain("Test Node")
|
||||
expect(firstNode?.title).toContain('Test Node')
|
||||
})
|
||||
|
||||
it("should maintain link integrity", () => {
|
||||
it('should maintain link integrity', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
nodeCount: 2,
|
||||
nodeCount: 2
|
||||
})
|
||||
|
||||
const node1 = subgraph.nodes[0]
|
||||
|
||||
@@ -4,21 +4,20 @@
|
||||
* Tests for edge cases, error handling, and boundary conditions in the subgraph system.
|
||||
* This covers unusual scenarios, invalid states, and stress testing.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { LGraph, LGraphNode, Subgraph } from "@/lib/litegraph/src/litegraph"
|
||||
import { LGraph, LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import {
|
||||
createNestedSubgraphs,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
} from "./fixtures/subgraphHelpers"
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("SubgraphEdgeCases - Recursion Detection", () => {
|
||||
it("should handle circular subgraph references without crashing", () => {
|
||||
const sub1 = createTestSubgraph({ name: "Sub1" })
|
||||
const sub2 = createTestSubgraph({ name: "Sub2" })
|
||||
describe('SubgraphEdgeCases - Recursion Detection', () => {
|
||||
it('should handle circular subgraph references without crashing', () => {
|
||||
const sub1 = createTestSubgraph({ name: 'Sub1' })
|
||||
const sub2 = createTestSubgraph({ name: 'Sub2' })
|
||||
|
||||
// Create circular reference
|
||||
const node1 = createTestSubgraphNode(sub1, { id: 1 })
|
||||
@@ -34,7 +33,7 @@ describe("SubgraphEdgeCases - Recursion Detection", () => {
|
||||
}).toThrow(/Node \[\d+\] not found/) // Current behavior: path resolution fails
|
||||
})
|
||||
|
||||
it("should handle deep nesting scenarios", () => {
|
||||
it('should handle deep nesting scenarios', () => {
|
||||
// Test with reasonable depth to avoid timeout
|
||||
const nested = createNestedSubgraphs({ depth: 10, nodesPerLevel: 1 })
|
||||
|
||||
@@ -48,7 +47,7 @@ describe("SubgraphEdgeCases - Recursion Detection", () => {
|
||||
expect(firstLevel.isSubgraphNode()).toBe(true)
|
||||
})
|
||||
|
||||
it.todo("should use WeakSet for cycle detection", () => {
|
||||
it.todo('should use WeakSet for cycle detection', () => {
|
||||
// TODO: This test is currently skipped because cycle detection has a bug
|
||||
// The fix is to pass 'visited' directly instead of 'new Set(visited)' in SubgraphNode.ts:299
|
||||
const subgraph = createTestSubgraph({ nodeCount: 1 })
|
||||
@@ -64,10 +63,10 @@ describe("SubgraphEdgeCases - Recursion Detection", () => {
|
||||
}).toThrow(/while flattening subgraph/i)
|
||||
})
|
||||
|
||||
it("should respect MAX_NESTED_SUBGRAPHS constant", () => {
|
||||
it('should respect MAX_NESTED_SUBGRAPHS constant', () => {
|
||||
// Verify the constant exists and is a reasonable positive number
|
||||
expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBeDefined()
|
||||
expect(typeof Subgraph.MAX_NESTED_SUBGRAPHS).toBe("number")
|
||||
expect(typeof Subgraph.MAX_NESTED_SUBGRAPHS).toBe('number')
|
||||
expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBeGreaterThan(0)
|
||||
expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBeLessThanOrEqual(10_000) // Reasonable upper bound
|
||||
|
||||
@@ -76,10 +75,14 @@ describe("SubgraphEdgeCases - Recursion Detection", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphEdgeCases - Invalid States", () => {
|
||||
it("should handle removing non-existent inputs gracefully", () => {
|
||||
describe('SubgraphEdgeCases - Invalid States', () => {
|
||||
it('should handle removing non-existent inputs gracefully', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const fakeInput = { name: "fake", type: "number", disconnect: () => {} } as any
|
||||
const fakeInput = {
|
||||
name: 'fake',
|
||||
type: 'number',
|
||||
disconnect: () => {}
|
||||
} as any
|
||||
|
||||
// Should throw appropriate error for non-existent input
|
||||
expect(() => {
|
||||
@@ -87,92 +90,96 @@ describe("SubgraphEdgeCases - Invalid States", () => {
|
||||
}).toThrow(/Input not found/) // Expected error
|
||||
})
|
||||
|
||||
it("should handle removing non-existent outputs gracefully", () => {
|
||||
it('should handle removing non-existent outputs gracefully', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const fakeOutput = { name: "fake", type: "number", disconnect: () => {} } as any
|
||||
const fakeOutput = {
|
||||
name: 'fake',
|
||||
type: 'number',
|
||||
disconnect: () => {}
|
||||
} as any
|
||||
|
||||
expect(() => {
|
||||
subgraph.removeOutput(fakeOutput)
|
||||
}).toThrow(/Output not found/) // Expected error
|
||||
})
|
||||
|
||||
it("should handle null/undefined input names", () => {
|
||||
it('should handle null/undefined input names', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// ISSUE: Current implementation allows null/undefined names which may cause runtime errors
|
||||
// TODO: Consider adding validation to prevent null/undefined names
|
||||
// This test documents the current permissive behavior
|
||||
expect(() => {
|
||||
subgraph.addInput(null as any, "number")
|
||||
subgraph.addInput(null as any, 'number')
|
||||
}).not.toThrow() // Current behavior: allows null
|
||||
|
||||
expect(() => {
|
||||
subgraph.addInput(undefined as any, "number")
|
||||
subgraph.addInput(undefined as any, 'number')
|
||||
}).not.toThrow() // Current behavior: allows undefined
|
||||
})
|
||||
|
||||
it("should handle null/undefined output names", () => {
|
||||
it('should handle null/undefined output names', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// ISSUE: Current implementation allows null/undefined names which may cause runtime errors
|
||||
// TODO: Consider adding validation to prevent null/undefined names
|
||||
// This test documents the current permissive behavior
|
||||
expect(() => {
|
||||
subgraph.addOutput(null as any, "number")
|
||||
subgraph.addOutput(null as any, 'number')
|
||||
}).not.toThrow() // Current behavior: allows null
|
||||
|
||||
expect(() => {
|
||||
subgraph.addOutput(undefined as any, "number")
|
||||
subgraph.addOutput(undefined as any, 'number')
|
||||
}).not.toThrow() // Current behavior: allows undefined
|
||||
})
|
||||
|
||||
it("should handle empty string names", () => {
|
||||
it('should handle empty string names', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// Current implementation may allow empty strings
|
||||
// Document the actual behavior
|
||||
expect(() => {
|
||||
subgraph.addInput("", "number")
|
||||
subgraph.addInput('', 'number')
|
||||
}).not.toThrow() // Current behavior: allows empty strings
|
||||
|
||||
expect(() => {
|
||||
subgraph.addOutput("", "number")
|
||||
subgraph.addOutput('', 'number')
|
||||
}).not.toThrow() // Current behavior: allows empty strings
|
||||
})
|
||||
|
||||
it("should handle undefined types gracefully", () => {
|
||||
it('should handle undefined types gracefully', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// Undefined type should not crash but may have default behavior
|
||||
expect(() => {
|
||||
subgraph.addInput("test", undefined as any)
|
||||
subgraph.addInput('test', undefined as any)
|
||||
}).not.toThrow()
|
||||
|
||||
expect(() => {
|
||||
subgraph.addOutput("test", undefined as any)
|
||||
subgraph.addOutput('test', undefined as any)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it("should handle duplicate slot names", () => {
|
||||
it('should handle duplicate slot names', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// Add first input
|
||||
subgraph.addInput("duplicate", "number")
|
||||
subgraph.addInput('duplicate', 'number')
|
||||
|
||||
// Adding duplicate should not crash (current behavior allows it)
|
||||
expect(() => {
|
||||
subgraph.addInput("duplicate", "string")
|
||||
subgraph.addInput('duplicate', 'string')
|
||||
}).not.toThrow()
|
||||
|
||||
// Should now have 2 inputs with same name
|
||||
expect(subgraph.inputs.length).toBe(2)
|
||||
expect(subgraph.inputs[0].name).toBe("duplicate")
|
||||
expect(subgraph.inputs[1].name).toBe("duplicate")
|
||||
expect(subgraph.inputs[0].name).toBe('duplicate')
|
||||
expect(subgraph.inputs[1].name).toBe('duplicate')
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphEdgeCases - Boundary Conditions", () => {
|
||||
it("should handle empty subgraphs (no nodes, no IO)", () => {
|
||||
describe('SubgraphEdgeCases - Boundary Conditions', () => {
|
||||
it('should handle empty subgraphs (no nodes, no IO)', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 0 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -185,32 +192,32 @@ describe("SubgraphEdgeCases - Boundary Conditions", () => {
|
||||
expect(subgraph.outputs).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle single input/output subgraphs", () => {
|
||||
it('should handle single input/output subgraphs', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "single_in", type: "number" }],
|
||||
outputs: [{ name: "single_out", type: "number" }],
|
||||
nodeCount: 1,
|
||||
inputs: [{ name: 'single_in', type: 'number' }],
|
||||
outputs: [{ name: 'single_out', type: 'number' }],
|
||||
nodeCount: 1
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
expect(subgraphNode.inputs).toHaveLength(1)
|
||||
expect(subgraphNode.outputs).toHaveLength(1)
|
||||
expect(subgraphNode.inputs[0].name).toBe("single_in")
|
||||
expect(subgraphNode.outputs[0].name).toBe("single_out")
|
||||
expect(subgraphNode.inputs[0].name).toBe('single_in')
|
||||
expect(subgraphNode.outputs[0].name).toBe('single_out')
|
||||
})
|
||||
|
||||
it("should handle subgraphs with many slots", () => {
|
||||
it('should handle subgraphs with many slots', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 1 })
|
||||
|
||||
// Add many inputs (test with 20 to keep test fast)
|
||||
for (let i = 0; i < 20; i++) {
|
||||
subgraph.addInput(`input_${i}`, "number")
|
||||
subgraph.addInput(`input_${i}`, 'number')
|
||||
}
|
||||
|
||||
// Add many outputs
|
||||
for (let i = 0; i < 20; i++) {
|
||||
subgraph.addOutput(`output_${i}`, "number")
|
||||
subgraph.addOutput(`output_${i}`, 'number')
|
||||
}
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -226,26 +233,26 @@ describe("SubgraphEdgeCases - Boundary Conditions", () => {
|
||||
expect(flattened).toHaveLength(1) // Original node count
|
||||
})
|
||||
|
||||
it("should handle very long slot names", () => {
|
||||
it('should handle very long slot names', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const longName = "a".repeat(1000) // 1000 character name
|
||||
const longName = 'a'.repeat(1000) // 1000 character name
|
||||
|
||||
expect(() => {
|
||||
subgraph.addInput(longName, "number")
|
||||
subgraph.addOutput(longName, "string")
|
||||
subgraph.addInput(longName, 'number')
|
||||
subgraph.addOutput(longName, 'string')
|
||||
}).not.toThrow()
|
||||
|
||||
expect(subgraph.inputs[0].name).toBe(longName)
|
||||
expect(subgraph.outputs[0].name).toBe(longName)
|
||||
})
|
||||
|
||||
it("should handle Unicode characters in names", () => {
|
||||
it('should handle Unicode characters in names', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const unicodeName = "测试_🚀_تست_тест"
|
||||
const unicodeName = '测试_🚀_تست_тест'
|
||||
|
||||
expect(() => {
|
||||
subgraph.addInput(unicodeName, "number")
|
||||
subgraph.addOutput(unicodeName, "string")
|
||||
subgraph.addInput(unicodeName, 'number')
|
||||
subgraph.addOutput(unicodeName, 'string')
|
||||
}).not.toThrow()
|
||||
|
||||
expect(subgraph.inputs[0].name).toBe(unicodeName)
|
||||
@@ -253,17 +260,17 @@ describe("SubgraphEdgeCases - Boundary Conditions", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphEdgeCases - Type Validation", () => {
|
||||
it("should allow connecting mismatched types (no validation currently)", () => {
|
||||
describe('SubgraphEdgeCases - Type Validation', () => {
|
||||
it('should allow connecting mismatched types (no validation currently)', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
subgraph.addInput("num", "number")
|
||||
subgraph.addOutput("str", "string")
|
||||
subgraph.addInput('num', 'number')
|
||||
subgraph.addOutput('str', 'string')
|
||||
|
||||
// Create a basic node manually since createNode is not available
|
||||
const numberNode = new LGraphNode("basic/const")
|
||||
numberNode.addOutput("value", "number")
|
||||
const numberNode = new LGraphNode('basic/const')
|
||||
numberNode.addOutput('value', 'number')
|
||||
rootGraph.add(numberNode)
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -275,36 +282,36 @@ describe("SubgraphEdgeCases - Type Validation", () => {
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it("should handle invalid type strings", () => {
|
||||
it('should handle invalid type strings', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// These should not crash (current behavior)
|
||||
expect(() => {
|
||||
subgraph.addInput("test1", "invalid_type")
|
||||
subgraph.addInput("test2", "")
|
||||
subgraph.addInput("test3", "123")
|
||||
subgraph.addInput("test4", "special!@#$%")
|
||||
subgraph.addInput('test1', 'invalid_type')
|
||||
subgraph.addInput('test2', '')
|
||||
subgraph.addInput('test3', '123')
|
||||
subgraph.addInput('test4', 'special!@#$%')
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it("should handle complex type strings", () => {
|
||||
it('should handle complex type strings', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
expect(() => {
|
||||
subgraph.addInput("array", "array<number>")
|
||||
subgraph.addInput("object", "object<{x: number, y: string}>")
|
||||
subgraph.addInput("union", "number|string")
|
||||
subgraph.addInput('array', 'array<number>')
|
||||
subgraph.addInput('object', 'object<{x: number, y: string}>')
|
||||
subgraph.addInput('union', 'number|string')
|
||||
}).not.toThrow()
|
||||
|
||||
expect(subgraph.inputs).toHaveLength(3)
|
||||
expect(subgraph.inputs[0].type).toBe("array<number>")
|
||||
expect(subgraph.inputs[1].type).toBe("object<{x: number, y: string}>")
|
||||
expect(subgraph.inputs[2].type).toBe("number|string")
|
||||
expect(subgraph.inputs[0].type).toBe('array<number>')
|
||||
expect(subgraph.inputs[1].type).toBe('object<{x: number, y: string}>')
|
||||
expect(subgraph.inputs[2].type).toBe('number|string')
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphEdgeCases - Performance and Scale", () => {
|
||||
it("should handle large numbers of nodes in subgraph", () => {
|
||||
describe('SubgraphEdgeCases - Performance and Scale', () => {
|
||||
it('should handle large numbers of nodes in subgraph', () => {
|
||||
// Create subgraph with many nodes (keep reasonable for test speed)
|
||||
const subgraph = createTestSubgraph({ nodeCount: 50 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -317,13 +324,13 @@ describe("SubgraphEdgeCases - Performance and Scale", () => {
|
||||
// Performance is acceptable for 50 nodes (typically < 1ms)
|
||||
})
|
||||
|
||||
it("should handle rapid IO changes", () => {
|
||||
it('should handle rapid IO changes', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// Rapidly add and remove inputs/outputs
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const input = subgraph.addInput(`rapid_${i}`, "number")
|
||||
const output = subgraph.addOutput(`rapid_${i}`, "number")
|
||||
const input = subgraph.addInput(`rapid_${i}`, 'number')
|
||||
const output = subgraph.addOutput(`rapid_${i}`, 'number')
|
||||
|
||||
// Remove them immediately
|
||||
subgraph.removeInput(input)
|
||||
@@ -335,7 +342,7 @@ describe("SubgraphEdgeCases - Performance and Scale", () => {
|
||||
expect(subgraph.outputs).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle concurrent modifications safely", () => {
|
||||
it('should handle concurrent modifications safely', () => {
|
||||
// This test ensures the system doesn't crash under concurrent access
|
||||
// Note: JavaScript is single-threaded, so this tests rapid sequential access
|
||||
const subgraph = createTestSubgraph({ nodeCount: 5 })
|
||||
@@ -344,16 +351,20 @@ describe("SubgraphEdgeCases - Performance and Scale", () => {
|
||||
// Simulate concurrent operations
|
||||
const operations = []
|
||||
for (let i = 0; i < 20; i++) {
|
||||
operations.push(() => {
|
||||
const executableNodes = new Map()
|
||||
subgraphNode.getInnerNodes(executableNodes)
|
||||
}, () => {
|
||||
subgraph.addInput(`concurrent_${i}`, "number")
|
||||
}, () => {
|
||||
if (subgraph.inputs.length > 0) {
|
||||
subgraph.removeInput(subgraph.inputs[0])
|
||||
operations.push(
|
||||
() => {
|
||||
const executableNodes = new Map()
|
||||
subgraphNode.getInnerNodes(executableNodes)
|
||||
},
|
||||
() => {
|
||||
subgraph.addInput(`concurrent_${i}`, 'number')
|
||||
},
|
||||
() => {
|
||||
if (subgraph.inputs.length > 0) {
|
||||
subgraph.removeInput(subgraph.inputs[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Execute all operations - should not crash
|
||||
|
||||
@@ -1,389 +1,440 @@
|
||||
import { describe, expect, vi } from "vitest"
|
||||
import { describe, expect, vi } from 'vitest'
|
||||
|
||||
import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
import { verifyEventSequence } from "./fixtures/subgraphHelpers"
|
||||
import { subgraphTest } from './fixtures/subgraphFixtures'
|
||||
import { verifyEventSequence } from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("SubgraphEvents - Event Payload Verification", () => {
|
||||
subgraphTest("dispatches input-added with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
describe('SubgraphEvents - Event Payload Verification', () => {
|
||||
subgraphTest(
|
||||
'dispatches input-added with correct payload',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const input = subgraph.addInput("test_input", "number")
|
||||
const input = subgraph.addInput('test_input', 'number')
|
||||
|
||||
const addedEvents = capture.getEventsByType("input-added")
|
||||
expect(addedEvents).toHaveLength(1)
|
||||
const addedEvents = capture.getEventsByType('input-added')
|
||||
expect(addedEvents).toHaveLength(1)
|
||||
|
||||
expect(addedEvents[0].detail).toEqual({
|
||||
input: expect.objectContaining({
|
||||
name: "test_input",
|
||||
type: "number",
|
||||
}),
|
||||
})
|
||||
expect(addedEvents[0].detail).toEqual({
|
||||
input: expect.objectContaining({
|
||||
name: 'test_input',
|
||||
type: 'number'
|
||||
})
|
||||
})
|
||||
|
||||
expect(addedEvents[0].detail.input).toBe(input)
|
||||
})
|
||||
expect(addedEvents[0].detail.input).toBe(input)
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("dispatches output-added with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
subgraphTest(
|
||||
'dispatches output-added with correct payload',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const output = subgraph.addOutput("test_output", "string")
|
||||
const output = subgraph.addOutput('test_output', 'string')
|
||||
|
||||
const addedEvents = capture.getEventsByType("output-added")
|
||||
expect(addedEvents).toHaveLength(1)
|
||||
const addedEvents = capture.getEventsByType('output-added')
|
||||
expect(addedEvents).toHaveLength(1)
|
||||
|
||||
expect(addedEvents[0].detail).toEqual({
|
||||
output: expect.objectContaining({
|
||||
name: "test_output",
|
||||
type: "string",
|
||||
}),
|
||||
})
|
||||
expect(addedEvents[0].detail).toEqual({
|
||||
output: expect.objectContaining({
|
||||
name: 'test_output',
|
||||
type: 'string'
|
||||
})
|
||||
})
|
||||
|
||||
expect(addedEvents[0].detail.output).toBe(output)
|
||||
})
|
||||
expect(addedEvents[0].detail.output).toBe(output)
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("dispatches removing-input with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
subgraphTest(
|
||||
'dispatches removing-input with correct payload',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const input = subgraph.addInput("to_remove", "boolean")
|
||||
const input = subgraph.addInput('to_remove', 'boolean')
|
||||
|
||||
capture.clear()
|
||||
capture.clear()
|
||||
|
||||
subgraph.removeInput(input)
|
||||
subgraph.removeInput(input)
|
||||
|
||||
const removingEvents = capture.getEventsByType("removing-input")
|
||||
expect(removingEvents).toHaveLength(1)
|
||||
const removingEvents = capture.getEventsByType('removing-input')
|
||||
expect(removingEvents).toHaveLength(1)
|
||||
|
||||
expect(removingEvents[0].detail).toEqual({
|
||||
input: expect.objectContaining({
|
||||
name: "to_remove",
|
||||
type: "boolean",
|
||||
}),
|
||||
index: 0,
|
||||
})
|
||||
expect(removingEvents[0].detail).toEqual({
|
||||
input: expect.objectContaining({
|
||||
name: 'to_remove',
|
||||
type: 'boolean'
|
||||
}),
|
||||
index: 0
|
||||
})
|
||||
|
||||
expect(removingEvents[0].detail.input).toBe(input)
|
||||
})
|
||||
expect(removingEvents[0].detail.input).toBe(input)
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("dispatches removing-output with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
subgraphTest(
|
||||
'dispatches removing-output with correct payload',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const output = subgraph.addOutput("to_remove", "number")
|
||||
const output = subgraph.addOutput('to_remove', 'number')
|
||||
|
||||
capture.clear()
|
||||
capture.clear()
|
||||
|
||||
subgraph.removeOutput(output)
|
||||
subgraph.removeOutput(output)
|
||||
|
||||
const removingEvents = capture.getEventsByType("removing-output")
|
||||
expect(removingEvents).toHaveLength(1)
|
||||
const removingEvents = capture.getEventsByType('removing-output')
|
||||
expect(removingEvents).toHaveLength(1)
|
||||
|
||||
expect(removingEvents[0].detail).toEqual({
|
||||
output: expect.objectContaining({
|
||||
name: "to_remove",
|
||||
type: "number",
|
||||
}),
|
||||
index: 0,
|
||||
})
|
||||
expect(removingEvents[0].detail).toEqual({
|
||||
output: expect.objectContaining({
|
||||
name: 'to_remove',
|
||||
type: 'number'
|
||||
}),
|
||||
index: 0
|
||||
})
|
||||
|
||||
expect(removingEvents[0].detail.output).toBe(output)
|
||||
})
|
||||
expect(removingEvents[0].detail.output).toBe(output)
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("dispatches renaming-input with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
subgraphTest(
|
||||
'dispatches renaming-input with correct payload',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const input = subgraph.addInput("old_name", "string")
|
||||
const input = subgraph.addInput('old_name', 'string')
|
||||
|
||||
capture.clear()
|
||||
capture.clear()
|
||||
|
||||
subgraph.renameInput(input, "new_name")
|
||||
subgraph.renameInput(input, 'new_name')
|
||||
|
||||
const renamingEvents = capture.getEventsByType("renaming-input")
|
||||
expect(renamingEvents).toHaveLength(1)
|
||||
const renamingEvents = capture.getEventsByType('renaming-input')
|
||||
expect(renamingEvents).toHaveLength(1)
|
||||
|
||||
expect(renamingEvents[0].detail).toEqual({
|
||||
input: expect.objectContaining({
|
||||
type: "string",
|
||||
}),
|
||||
index: 0,
|
||||
oldName: "old_name",
|
||||
newName: "new_name",
|
||||
})
|
||||
expect(renamingEvents[0].detail).toEqual({
|
||||
input: expect.objectContaining({
|
||||
type: 'string'
|
||||
}),
|
||||
index: 0,
|
||||
oldName: 'old_name',
|
||||
newName: 'new_name'
|
||||
})
|
||||
|
||||
expect(renamingEvents[0].detail.input).toBe(input)
|
||||
expect(renamingEvents[0].detail.input).toBe(input)
|
||||
|
||||
// Verify the label was updated after the event (renameInput sets label, not name)
|
||||
expect(input.label).toBe("new_name")
|
||||
expect(input.displayName).toBe("new_name")
|
||||
expect(input.name).toBe("old_name")
|
||||
})
|
||||
// Verify the label was updated after the event (renameInput sets label, not name)
|
||||
expect(input.label).toBe('new_name')
|
||||
expect(input.displayName).toBe('new_name')
|
||||
expect(input.name).toBe('old_name')
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("dispatches renaming-output with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
subgraphTest(
|
||||
'dispatches renaming-output with correct payload',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const output = subgraph.addOutput("old_name", "number")
|
||||
const output = subgraph.addOutput('old_name', 'number')
|
||||
|
||||
capture.clear()
|
||||
capture.clear()
|
||||
|
||||
subgraph.renameOutput(output, "new_name")
|
||||
subgraph.renameOutput(output, 'new_name')
|
||||
|
||||
const renamingEvents = capture.getEventsByType("renaming-output")
|
||||
expect(renamingEvents).toHaveLength(1)
|
||||
const renamingEvents = capture.getEventsByType('renaming-output')
|
||||
expect(renamingEvents).toHaveLength(1)
|
||||
|
||||
expect(renamingEvents[0].detail).toEqual({
|
||||
output: expect.objectContaining({
|
||||
name: "old_name", // Should still have the old name when event is dispatched
|
||||
type: "number",
|
||||
}),
|
||||
index: 0,
|
||||
oldName: "old_name",
|
||||
newName: "new_name",
|
||||
})
|
||||
expect(renamingEvents[0].detail).toEqual({
|
||||
output: expect.objectContaining({
|
||||
name: 'old_name', // Should still have the old name when event is dispatched
|
||||
type: 'number'
|
||||
}),
|
||||
index: 0,
|
||||
oldName: 'old_name',
|
||||
newName: 'new_name'
|
||||
})
|
||||
|
||||
expect(renamingEvents[0].detail.output).toBe(output)
|
||||
expect(renamingEvents[0].detail.output).toBe(output)
|
||||
|
||||
// Verify the label was updated after the event
|
||||
expect(output.label).toBe("new_name")
|
||||
expect(output.displayName).toBe("new_name")
|
||||
expect(output.name).toBe("old_name")
|
||||
})
|
||||
// Verify the label was updated after the event
|
||||
expect(output.label).toBe('new_name')
|
||||
expect(output.displayName).toBe('new_name')
|
||||
expect(output.name).toBe('old_name')
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("dispatches adding-input with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
subgraphTest(
|
||||
'dispatches adding-input with correct payload',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addInput("test_input", "number")
|
||||
subgraph.addInput('test_input', 'number')
|
||||
|
||||
const addingEvents = capture.getEventsByType("adding-input")
|
||||
expect(addingEvents).toHaveLength(1)
|
||||
const addingEvents = capture.getEventsByType('adding-input')
|
||||
expect(addingEvents).toHaveLength(1)
|
||||
|
||||
expect(addingEvents[0].detail).toEqual({
|
||||
name: "test_input",
|
||||
type: "number",
|
||||
})
|
||||
})
|
||||
expect(addingEvents[0].detail).toEqual({
|
||||
name: 'test_input',
|
||||
type: 'number'
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("dispatches adding-output with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
subgraphTest(
|
||||
'dispatches adding-output with correct payload',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addOutput("test_output", "string")
|
||||
subgraph.addOutput('test_output', 'string')
|
||||
|
||||
const addingEvents = capture.getEventsByType("adding-output")
|
||||
expect(addingEvents).toHaveLength(1)
|
||||
const addingEvents = capture.getEventsByType('adding-output')
|
||||
expect(addingEvents).toHaveLength(1)
|
||||
|
||||
expect(addingEvents[0].detail).toEqual({
|
||||
name: "test_output",
|
||||
type: "string",
|
||||
})
|
||||
})
|
||||
expect(addingEvents[0].detail).toEqual({
|
||||
name: 'test_output',
|
||||
type: 'string'
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe("SubgraphEvents - Event Handler Isolation", () => {
|
||||
subgraphTest("continues dispatching if handler throws", ({ emptySubgraph }) => {
|
||||
const handler1 = vi.fn(() => {
|
||||
throw new Error("Handler 1 error")
|
||||
})
|
||||
const handler2 = vi.fn()
|
||||
const handler3 = vi.fn()
|
||||
describe('SubgraphEvents - Event Handler Isolation', () => {
|
||||
subgraphTest(
|
||||
'continues dispatching if handler throws',
|
||||
({ emptySubgraph }) => {
|
||||
const handler1 = vi.fn(() => {
|
||||
throw new Error('Handler 1 error')
|
||||
})
|
||||
const handler2 = vi.fn()
|
||||
const handler3 = vi.fn()
|
||||
|
||||
emptySubgraph.events.addEventListener("input-added", handler1)
|
||||
emptySubgraph.events.addEventListener("input-added", handler2)
|
||||
emptySubgraph.events.addEventListener("input-added", handler3)
|
||||
emptySubgraph.events.addEventListener('input-added', handler1)
|
||||
emptySubgraph.events.addEventListener('input-added', handler2)
|
||||
emptySubgraph.events.addEventListener('input-added', handler3)
|
||||
|
||||
// The operation itself should not throw (error is isolated)
|
||||
expect(() => {
|
||||
emptySubgraph.addInput("test", "number")
|
||||
}).not.toThrow()
|
||||
// The operation itself should not throw (error is isolated)
|
||||
expect(() => {
|
||||
emptySubgraph.addInput('test', 'number')
|
||||
}).not.toThrow()
|
||||
|
||||
// Verify all handlers were called despite the first one throwing
|
||||
expect(handler1).toHaveBeenCalled()
|
||||
expect(handler2).toHaveBeenCalled()
|
||||
expect(handler3).toHaveBeenCalled()
|
||||
// Verify all handlers were called despite the first one throwing
|
||||
expect(handler1).toHaveBeenCalled()
|
||||
expect(handler2).toHaveBeenCalled()
|
||||
expect(handler3).toHaveBeenCalled()
|
||||
|
||||
// Verify the throwing handler actually received the event
|
||||
expect(handler1).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: "input-added",
|
||||
}))
|
||||
// Verify the throwing handler actually received the event
|
||||
expect(handler1).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'input-added'
|
||||
})
|
||||
)
|
||||
|
||||
// Verify other handlers received correct event data
|
||||
expect(handler2).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: "input-added",
|
||||
detail: expect.objectContaining({
|
||||
input: expect.objectContaining({
|
||||
name: "test",
|
||||
type: "number",
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
expect(handler3).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: "input-added",
|
||||
}))
|
||||
})
|
||||
// Verify other handlers received correct event data
|
||||
expect(handler2).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'input-added',
|
||||
detail: expect.objectContaining({
|
||||
input: expect.objectContaining({
|
||||
name: 'test',
|
||||
type: 'number'
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
expect(handler3).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'input-added'
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("maintains handler execution order", ({ emptySubgraph }) => {
|
||||
subgraphTest('maintains handler execution order', ({ emptySubgraph }) => {
|
||||
const executionOrder: number[] = []
|
||||
|
||||
const handler1 = vi.fn(() => executionOrder.push(1))
|
||||
const handler2 = vi.fn(() => executionOrder.push(2))
|
||||
const handler3 = vi.fn(() => executionOrder.push(3))
|
||||
|
||||
emptySubgraph.events.addEventListener("input-added", handler1)
|
||||
emptySubgraph.events.addEventListener("input-added", handler2)
|
||||
emptySubgraph.events.addEventListener("input-added", handler3)
|
||||
emptySubgraph.events.addEventListener('input-added', handler1)
|
||||
emptySubgraph.events.addEventListener('input-added', handler2)
|
||||
emptySubgraph.events.addEventListener('input-added', handler3)
|
||||
|
||||
emptySubgraph.addInput("test", "number")
|
||||
emptySubgraph.addInput('test', 'number')
|
||||
|
||||
expect(executionOrder).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
subgraphTest("prevents handler accumulation with proper cleanup", ({ emptySubgraph }) => {
|
||||
const handler = vi.fn()
|
||||
subgraphTest(
|
||||
'prevents handler accumulation with proper cleanup',
|
||||
({ emptySubgraph }) => {
|
||||
const handler = vi.fn()
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
emptySubgraph.events.addEventListener("input-added", handler)
|
||||
emptySubgraph.events.removeEventListener("input-added", handler)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
emptySubgraph.events.addEventListener('input-added', handler)
|
||||
emptySubgraph.events.removeEventListener('input-added', handler)
|
||||
}
|
||||
|
||||
emptySubgraph.events.addEventListener('input-added', handler)
|
||||
|
||||
emptySubgraph.addInput('test', 'number')
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
)
|
||||
|
||||
emptySubgraph.events.addEventListener("input-added", handler)
|
||||
subgraphTest(
|
||||
'supports AbortController cleanup patterns',
|
||||
({ emptySubgraph }) => {
|
||||
const abortController = new AbortController()
|
||||
const { signal } = abortController
|
||||
|
||||
emptySubgraph.addInput("test", "number")
|
||||
const handler = vi.fn()
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
emptySubgraph.events.addEventListener('input-added', handler, { signal })
|
||||
|
||||
subgraphTest("supports AbortController cleanup patterns", ({ emptySubgraph }) => {
|
||||
const abortController = new AbortController()
|
||||
const { signal } = abortController
|
||||
emptySubgraph.addInput('test1', 'number')
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
|
||||
const handler = vi.fn()
|
||||
abortController.abort()
|
||||
|
||||
emptySubgraph.events.addEventListener("input-added", handler, { signal })
|
||||
|
||||
emptySubgraph.addInput("test1", "number")
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
|
||||
abortController.abort()
|
||||
|
||||
emptySubgraph.addInput("test2", "number")
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
emptySubgraph.addInput('test2', 'number')
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe("SubgraphEvents - Event Sequence Testing", () => {
|
||||
subgraphTest("maintains correct event sequence for inputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
describe('SubgraphEvents - Event Sequence Testing', () => {
|
||||
subgraphTest(
|
||||
'maintains correct event sequence for inputs',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addInput("input1", "number")
|
||||
subgraph.addInput('input1', 'number')
|
||||
|
||||
verifyEventSequence(capture.events, [
|
||||
"adding-input",
|
||||
"input-added",
|
||||
])
|
||||
})
|
||||
verifyEventSequence(capture.events, ['adding-input', 'input-added'])
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("maintains correct event sequence for outputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
subgraphTest(
|
||||
'maintains correct event sequence for outputs',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addOutput("output1", "string")
|
||||
subgraph.addOutput('output1', 'string')
|
||||
|
||||
verifyEventSequence(capture.events, [
|
||||
"adding-output",
|
||||
"output-added",
|
||||
])
|
||||
})
|
||||
verifyEventSequence(capture.events, ['adding-output', 'output-added'])
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("maintains correct event sequence for rapid operations", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
subgraphTest(
|
||||
'maintains correct event sequence for rapid operations',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addInput("input1", "number")
|
||||
subgraph.addInput("input2", "string")
|
||||
subgraph.addOutput("output1", "boolean")
|
||||
subgraph.addOutput("output2", "number")
|
||||
subgraph.addInput('input1', 'number')
|
||||
subgraph.addInput('input2', 'string')
|
||||
subgraph.addOutput('output1', 'boolean')
|
||||
subgraph.addOutput('output2', 'number')
|
||||
|
||||
verifyEventSequence(capture.events, [
|
||||
"adding-input",
|
||||
"input-added",
|
||||
"adding-input",
|
||||
"input-added",
|
||||
"adding-output",
|
||||
"output-added",
|
||||
"adding-output",
|
||||
"output-added",
|
||||
])
|
||||
})
|
||||
verifyEventSequence(capture.events, [
|
||||
'adding-input',
|
||||
'input-added',
|
||||
'adding-input',
|
||||
'input-added',
|
||||
'adding-output',
|
||||
'output-added',
|
||||
'adding-output',
|
||||
'output-added'
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("handles concurrent event handling", ({ eventCapture }) => {
|
||||
subgraphTest('handles concurrent event handling', ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const handler1 = vi.fn(() => {
|
||||
return new Promise(resolve => setTimeout(resolve, 1))
|
||||
return new Promise((resolve) => setTimeout(resolve, 1))
|
||||
})
|
||||
|
||||
const handler2 = vi.fn()
|
||||
const handler3 = vi.fn()
|
||||
|
||||
subgraph.events.addEventListener("input-added", handler1)
|
||||
subgraph.events.addEventListener("input-added", handler2)
|
||||
subgraph.events.addEventListener("input-added", handler3)
|
||||
subgraph.events.addEventListener('input-added', handler1)
|
||||
subgraph.events.addEventListener('input-added', handler2)
|
||||
subgraph.events.addEventListener('input-added', handler3)
|
||||
|
||||
subgraph.addInput("test", "number")
|
||||
subgraph.addInput('test', 'number')
|
||||
|
||||
expect(handler1).toHaveBeenCalled()
|
||||
expect(handler2).toHaveBeenCalled()
|
||||
expect(handler3).toHaveBeenCalled()
|
||||
|
||||
const addedEvents = capture.getEventsByType("input-added")
|
||||
const addedEvents = capture.getEventsByType('input-added')
|
||||
expect(addedEvents).toHaveLength(1)
|
||||
})
|
||||
|
||||
subgraphTest("validates event timestamps are properly ordered", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
subgraphTest(
|
||||
'validates event timestamps are properly ordered',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addInput("input1", "number")
|
||||
subgraph.addInput("input2", "string")
|
||||
subgraph.addOutput("output1", "boolean")
|
||||
subgraph.addInput('input1', 'number')
|
||||
subgraph.addInput('input2', 'string')
|
||||
subgraph.addOutput('output1', 'boolean')
|
||||
|
||||
for (let i = 1; i < capture.events.length; i++) {
|
||||
expect(capture.events[i].timestamp).toBeGreaterThanOrEqual(
|
||||
capture.events[i - 1].timestamp,
|
||||
)
|
||||
for (let i = 1; i < capture.events.length; i++) {
|
||||
expect(capture.events[i].timestamp).toBeGreaterThanOrEqual(
|
||||
capture.events[i - 1].timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe("SubgraphEvents - Event Cancellation", () => {
|
||||
subgraphTest("supports preventDefault() for cancellable events", ({ emptySubgraph }) => {
|
||||
const preventHandler = vi.fn((event: Event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
describe('SubgraphEvents - Event Cancellation', () => {
|
||||
subgraphTest(
|
||||
'supports preventDefault() for cancellable events',
|
||||
({ emptySubgraph }) => {
|
||||
const preventHandler = vi.fn((event: Event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
emptySubgraph.events.addEventListener("removing-input", preventHandler)
|
||||
emptySubgraph.events.addEventListener('removing-input', preventHandler)
|
||||
|
||||
const input = emptySubgraph.addInput("test", "number")
|
||||
const input = emptySubgraph.addInput('test', 'number')
|
||||
|
||||
emptySubgraph.removeInput(input)
|
||||
emptySubgraph.removeInput(input)
|
||||
|
||||
expect(emptySubgraph.inputs).toContain(input)
|
||||
expect(preventHandler).toHaveBeenCalled()
|
||||
})
|
||||
expect(emptySubgraph.inputs).toContain(input)
|
||||
expect(preventHandler).toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("supports preventDefault() for output removal", ({ emptySubgraph }) => {
|
||||
const preventHandler = vi.fn((event: Event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
subgraphTest(
|
||||
'supports preventDefault() for output removal',
|
||||
({ emptySubgraph }) => {
|
||||
const preventHandler = vi.fn((event: Event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
emptySubgraph.events.addEventListener("removing-output", preventHandler)
|
||||
emptySubgraph.events.addEventListener('removing-output', preventHandler)
|
||||
|
||||
const output = emptySubgraph.addOutput("test", "number")
|
||||
const output = emptySubgraph.addOutput('test', 'number')
|
||||
|
||||
emptySubgraph.removeOutput(output)
|
||||
emptySubgraph.removeOutput(output)
|
||||
|
||||
expect(emptySubgraph.outputs).toContain(output)
|
||||
expect(preventHandler).toHaveBeenCalled()
|
||||
})
|
||||
expect(emptySubgraph.outputs).toContain(output)
|
||||
expect(preventHandler).toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("allows removal when not prevented", ({ emptySubgraph }) => {
|
||||
subgraphTest('allows removal when not prevented', ({ emptySubgraph }) => {
|
||||
const allowHandler = vi.fn()
|
||||
|
||||
emptySubgraph.events.addEventListener("removing-input", allowHandler)
|
||||
emptySubgraph.events.addEventListener('removing-input', allowHandler)
|
||||
|
||||
const input = emptySubgraph.addInput("test", "number")
|
||||
const input = emptySubgraph.addInput('test', 'number')
|
||||
|
||||
emptySubgraph.removeInput(input)
|
||||
|
||||
@@ -393,66 +444,69 @@ describe("SubgraphEvents - Event Cancellation", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphEvents - Event Detail Structure Validation", () => {
|
||||
subgraphTest("validates all event detail structures match TypeScript types", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
describe('SubgraphEvents - Event Detail Structure Validation', () => {
|
||||
subgraphTest(
|
||||
'validates all event detail structures match TypeScript types',
|
||||
({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const input = subgraph.addInput("test_input", "number")
|
||||
subgraph.renameInput(input, "renamed_input")
|
||||
subgraph.removeInput(input)
|
||||
const input = subgraph.addInput('test_input', 'number')
|
||||
subgraph.renameInput(input, 'renamed_input')
|
||||
subgraph.removeInput(input)
|
||||
|
||||
const output = subgraph.addOutput("test_output", "string")
|
||||
subgraph.renameOutput(output, "renamed_output")
|
||||
subgraph.removeOutput(output)
|
||||
const output = subgraph.addOutput('test_output', 'string')
|
||||
subgraph.renameOutput(output, 'renamed_output')
|
||||
subgraph.removeOutput(output)
|
||||
|
||||
const addingInputEvent = capture.getEventsByType("adding-input")[0]
|
||||
expect(addingInputEvent.detail).toEqual({
|
||||
name: expect.any(String),
|
||||
type: expect.any(String),
|
||||
})
|
||||
const addingInputEvent = capture.getEventsByType('adding-input')[0]
|
||||
expect(addingInputEvent.detail).toEqual({
|
||||
name: expect.any(String),
|
||||
type: expect.any(String)
|
||||
})
|
||||
|
||||
const inputAddedEvent = capture.getEventsByType("input-added")[0]
|
||||
expect(inputAddedEvent.detail).toEqual({
|
||||
input: expect.any(Object),
|
||||
})
|
||||
const inputAddedEvent = capture.getEventsByType('input-added')[0]
|
||||
expect(inputAddedEvent.detail).toEqual({
|
||||
input: expect.any(Object)
|
||||
})
|
||||
|
||||
const renamingInputEvent = capture.getEventsByType("renaming-input")[0]
|
||||
expect(renamingInputEvent.detail).toEqual({
|
||||
input: expect.any(Object),
|
||||
index: expect.any(Number),
|
||||
oldName: expect.any(String),
|
||||
newName: expect.any(String),
|
||||
})
|
||||
const renamingInputEvent = capture.getEventsByType('renaming-input')[0]
|
||||
expect(renamingInputEvent.detail).toEqual({
|
||||
input: expect.any(Object),
|
||||
index: expect.any(Number),
|
||||
oldName: expect.any(String),
|
||||
newName: expect.any(String)
|
||||
})
|
||||
|
||||
const removingInputEvent = capture.getEventsByType("removing-input")[0]
|
||||
expect(removingInputEvent.detail).toEqual({
|
||||
input: expect.any(Object),
|
||||
index: expect.any(Number),
|
||||
})
|
||||
const removingInputEvent = capture.getEventsByType('removing-input')[0]
|
||||
expect(removingInputEvent.detail).toEqual({
|
||||
input: expect.any(Object),
|
||||
index: expect.any(Number)
|
||||
})
|
||||
|
||||
const addingOutputEvent = capture.getEventsByType("adding-output")[0]
|
||||
expect(addingOutputEvent.detail).toEqual({
|
||||
name: expect.any(String),
|
||||
type: expect.any(String),
|
||||
})
|
||||
const addingOutputEvent = capture.getEventsByType('adding-output')[0]
|
||||
expect(addingOutputEvent.detail).toEqual({
|
||||
name: expect.any(String),
|
||||
type: expect.any(String)
|
||||
})
|
||||
|
||||
const outputAddedEvent = capture.getEventsByType("output-added")[0]
|
||||
expect(outputAddedEvent.detail).toEqual({
|
||||
output: expect.any(Object),
|
||||
})
|
||||
const outputAddedEvent = capture.getEventsByType('output-added')[0]
|
||||
expect(outputAddedEvent.detail).toEqual({
|
||||
output: expect.any(Object)
|
||||
})
|
||||
|
||||
const renamingOutputEvent = capture.getEventsByType("renaming-output")[0]
|
||||
expect(renamingOutputEvent.detail).toEqual({
|
||||
output: expect.any(Object),
|
||||
index: expect.any(Number),
|
||||
oldName: expect.any(String),
|
||||
newName: expect.any(String),
|
||||
})
|
||||
const renamingOutputEvent = capture.getEventsByType('renaming-output')[0]
|
||||
expect(renamingOutputEvent.detail).toEqual({
|
||||
output: expect.any(Object),
|
||||
index: expect.any(Number),
|
||||
oldName: expect.any(String),
|
||||
newName: expect.any(String)
|
||||
})
|
||||
|
||||
const removingOutputEvent = capture.getEventsByType("removing-output")[0]
|
||||
expect(removingOutputEvent.detail).toEqual({
|
||||
output: expect.any(Object),
|
||||
index: expect.any(Number),
|
||||
})
|
||||
})
|
||||
const removingOutputEvent = capture.getEventsByType('removing-output')[0]
|
||||
expect(removingOutputEvent.detail).toEqual({
|
||||
output: expect.any(Object),
|
||||
index: expect.any(Number)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,278 +1,320 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraphNode } from "@/lib/litegraph/src/litegraph"
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
import { subgraphTest } from './fixtures/subgraphFixtures'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
} from "./fixtures/subgraphHelpers"
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("SubgraphIO - Input Slot Dual-Nature Behavior", () => {
|
||||
subgraphTest("input accepts external connections from parent graph", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
describe('SubgraphIO - Input Slot Dual-Nature Behavior', () => {
|
||||
subgraphTest(
|
||||
'input accepts external connections from parent graph',
|
||||
({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
|
||||
subgraph.addInput("test_input", "number")
|
||||
subgraph.addInput('test_input', 'number')
|
||||
|
||||
const externalNode = new LGraphNode("External Source")
|
||||
externalNode.addOutput("out", "number")
|
||||
parentGraph.add(externalNode)
|
||||
const externalNode = new LGraphNode('External Source')
|
||||
externalNode.addOutput('out', 'number')
|
||||
parentGraph.add(externalNode)
|
||||
|
||||
expect(() => {
|
||||
externalNode.connect(0, subgraphNode, 0)
|
||||
}).not.toThrow()
|
||||
|
||||
expect(
|
||||
externalNode.outputs[0].links?.includes(subgraphNode.inputs[0].link)
|
||||
).toBe(true)
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest(
|
||||
'empty input slot creation enables dynamic IO',
|
||||
({ simpleSubgraph }) => {
|
||||
const initialInputCount = simpleSubgraph.inputs.length
|
||||
|
||||
// Create empty input slot
|
||||
simpleSubgraph.addInput('', '*')
|
||||
|
||||
// Should create new input
|
||||
expect(simpleSubgraph.inputs.length).toBe(initialInputCount + 1)
|
||||
|
||||
// The empty slot should be configurable
|
||||
const emptyInput = simpleSubgraph.inputs.at(-1)
|
||||
expect(emptyInput.name).toBe('')
|
||||
expect(emptyInput.type).toBe('*')
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest(
|
||||
'handles slot removal with active connections',
|
||||
({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
|
||||
const externalNode = new LGraphNode('External Source')
|
||||
externalNode.addOutput('out', '*')
|
||||
parentGraph.add(externalNode)
|
||||
|
||||
expect(() => {
|
||||
externalNode.connect(0, subgraphNode, 0)
|
||||
}).not.toThrow()
|
||||
|
||||
expect(externalNode.outputs[0].links?.includes(subgraphNode.inputs[0].link)).toBe(true)
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
})
|
||||
// Verify connection exists
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
|
||||
subgraphTest("empty input slot creation enables dynamic IO", ({ simpleSubgraph }) => {
|
||||
const initialInputCount = simpleSubgraph.inputs.length
|
||||
// Remove the existing input (fixture creates one input)
|
||||
const inputToRemove = subgraph.inputs[0]
|
||||
subgraph.removeInput(inputToRemove)
|
||||
|
||||
// Create empty input slot
|
||||
simpleSubgraph.addInput("", "*")
|
||||
// Connection should be cleaned up
|
||||
expect(subgraphNode.inputs.length).toBe(0)
|
||||
expect(externalNode.outputs[0].links).toHaveLength(0)
|
||||
}
|
||||
)
|
||||
|
||||
// Should create new input
|
||||
expect(simpleSubgraph.inputs.length).toBe(initialInputCount + 1)
|
||||
subgraphTest(
|
||||
'handles slot renaming with active connections',
|
||||
({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
|
||||
// The empty slot should be configurable
|
||||
const emptyInput = simpleSubgraph.inputs.at(-1)
|
||||
expect(emptyInput.name).toBe("")
|
||||
expect(emptyInput.type).toBe("*")
|
||||
})
|
||||
const externalNode = new LGraphNode('External Source')
|
||||
externalNode.addOutput('out', '*')
|
||||
parentGraph.add(externalNode)
|
||||
|
||||
subgraphTest("handles slot removal with active connections", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
externalNode.connect(0, subgraphNode, 0)
|
||||
|
||||
const externalNode = new LGraphNode("External Source")
|
||||
externalNode.addOutput("out", "*")
|
||||
parentGraph.add(externalNode)
|
||||
// Verify connection exists
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
|
||||
externalNode.connect(0, subgraphNode, 0)
|
||||
// Rename the existing input (fixture creates input named "input")
|
||||
const inputToRename = subgraph.inputs[0]
|
||||
subgraph.renameInput(inputToRename, 'new_name')
|
||||
|
||||
// Verify connection exists
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
|
||||
// Remove the existing input (fixture creates one input)
|
||||
const inputToRemove = subgraph.inputs[0]
|
||||
subgraph.removeInput(inputToRemove)
|
||||
|
||||
// Connection should be cleaned up
|
||||
expect(subgraphNode.inputs.length).toBe(0)
|
||||
expect(externalNode.outputs[0].links).toHaveLength(0)
|
||||
})
|
||||
|
||||
subgraphTest("handles slot renaming with active connections", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
|
||||
const externalNode = new LGraphNode("External Source")
|
||||
externalNode.addOutput("out", "*")
|
||||
parentGraph.add(externalNode)
|
||||
|
||||
externalNode.connect(0, subgraphNode, 0)
|
||||
|
||||
// Verify connection exists
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
|
||||
// Rename the existing input (fixture creates input named "input")
|
||||
const inputToRename = subgraph.inputs[0]
|
||||
subgraph.renameInput(inputToRename, "new_name")
|
||||
|
||||
// Connection should persist and subgraph definition should be updated
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
expect(subgraph.inputs[0].label).toBe("new_name")
|
||||
expect(subgraph.inputs[0].displayName).toBe("new_name")
|
||||
})
|
||||
// Connection should persist and subgraph definition should be updated
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
expect(subgraph.inputs[0].label).toBe('new_name')
|
||||
expect(subgraph.inputs[0].displayName).toBe('new_name')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe("SubgraphIO - Output Slot Dual-Nature Behavior", () => {
|
||||
subgraphTest("output provides connections to parent graph", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
describe('SubgraphIO - Output Slot Dual-Nature Behavior', () => {
|
||||
subgraphTest(
|
||||
'output provides connections to parent graph',
|
||||
({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
|
||||
// Add an output to the subgraph
|
||||
subgraph.addOutput("test_output", "number")
|
||||
// Add an output to the subgraph
|
||||
subgraph.addOutput('test_output', 'number')
|
||||
|
||||
const externalNode = new LGraphNode("External Target")
|
||||
externalNode.addInput("in", "number")
|
||||
parentGraph.add(externalNode)
|
||||
const externalNode = new LGraphNode('External Target')
|
||||
externalNode.addInput('in', 'number')
|
||||
parentGraph.add(externalNode)
|
||||
|
||||
// External connection from subgraph output should work
|
||||
expect(() => {
|
||||
subgraphNode.connect(0, externalNode, 0)
|
||||
}).not.toThrow()
|
||||
|
||||
expect(
|
||||
subgraphNode.outputs[0].links?.includes(externalNode.inputs[0].link)
|
||||
).toBe(true)
|
||||
expect(externalNode.inputs[0].link).not.toBe(null)
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest(
|
||||
'empty output slot creation enables dynamic IO',
|
||||
({ simpleSubgraph }) => {
|
||||
const initialOutputCount = simpleSubgraph.outputs.length
|
||||
|
||||
// Create empty output slot
|
||||
simpleSubgraph.addOutput('', '*')
|
||||
|
||||
// Should create new output
|
||||
expect(simpleSubgraph.outputs.length).toBe(initialOutputCount + 1)
|
||||
|
||||
// The empty slot should be configurable
|
||||
const emptyOutput = simpleSubgraph.outputs.at(-1)
|
||||
expect(emptyOutput.name).toBe('')
|
||||
expect(emptyOutput.type).toBe('*')
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest(
|
||||
'handles slot removal with active connections',
|
||||
({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
|
||||
const externalNode = new LGraphNode('External Target')
|
||||
externalNode.addInput('in', '*')
|
||||
parentGraph.add(externalNode)
|
||||
|
||||
// External connection from subgraph output should work
|
||||
expect(() => {
|
||||
subgraphNode.connect(0, externalNode, 0)
|
||||
}).not.toThrow()
|
||||
|
||||
expect(subgraphNode.outputs[0].links?.includes(externalNode.inputs[0].link)).toBe(true)
|
||||
expect(externalNode.inputs[0].link).not.toBe(null)
|
||||
})
|
||||
// Verify connection exists
|
||||
expect(externalNode.inputs[0].link).not.toBe(null)
|
||||
|
||||
subgraphTest("empty output slot creation enables dynamic IO", ({ simpleSubgraph }) => {
|
||||
const initialOutputCount = simpleSubgraph.outputs.length
|
||||
// Remove the existing output (fixture creates one output)
|
||||
const outputToRemove = subgraph.outputs[0]
|
||||
subgraph.removeOutput(outputToRemove)
|
||||
|
||||
// Create empty output slot
|
||||
simpleSubgraph.addOutput("", "*")
|
||||
// Connection should be cleaned up
|
||||
expect(subgraphNode.outputs.length).toBe(0)
|
||||
expect(externalNode.inputs[0].link).toBe(null)
|
||||
}
|
||||
)
|
||||
|
||||
// Should create new output
|
||||
expect(simpleSubgraph.outputs.length).toBe(initialOutputCount + 1)
|
||||
subgraphTest(
|
||||
'handles slot renaming updates all references',
|
||||
({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
|
||||
// The empty slot should be configurable
|
||||
const emptyOutput = simpleSubgraph.outputs.at(-1)
|
||||
expect(emptyOutput.name).toBe("")
|
||||
expect(emptyOutput.type).toBe("*")
|
||||
})
|
||||
const externalNode = new LGraphNode('External Target')
|
||||
externalNode.addInput('in', '*')
|
||||
parentGraph.add(externalNode)
|
||||
|
||||
subgraphTest("handles slot removal with active connections", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
subgraphNode.connect(0, externalNode, 0)
|
||||
|
||||
const externalNode = new LGraphNode("External Target")
|
||||
externalNode.addInput("in", "*")
|
||||
parentGraph.add(externalNode)
|
||||
// Verify connection exists
|
||||
expect(externalNode.inputs[0].link).not.toBe(null)
|
||||
|
||||
subgraphNode.connect(0, externalNode, 0)
|
||||
// Rename the existing output (fixture creates output named "output")
|
||||
const outputToRename = subgraph.outputs[0]
|
||||
subgraph.renameOutput(outputToRename, 'new_name')
|
||||
|
||||
// Verify connection exists
|
||||
expect(externalNode.inputs[0].link).not.toBe(null)
|
||||
|
||||
// Remove the existing output (fixture creates one output)
|
||||
const outputToRemove = subgraph.outputs[0]
|
||||
subgraph.removeOutput(outputToRemove)
|
||||
|
||||
// Connection should be cleaned up
|
||||
expect(subgraphNode.outputs.length).toBe(0)
|
||||
expect(externalNode.inputs[0].link).toBe(null)
|
||||
})
|
||||
|
||||
subgraphTest("handles slot renaming updates all references", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode, parentGraph } = subgraphWithNode
|
||||
|
||||
const externalNode = new LGraphNode("External Target")
|
||||
externalNode.addInput("in", "*")
|
||||
parentGraph.add(externalNode)
|
||||
|
||||
subgraphNode.connect(0, externalNode, 0)
|
||||
|
||||
// Verify connection exists
|
||||
expect(externalNode.inputs[0].link).not.toBe(null)
|
||||
|
||||
// Rename the existing output (fixture creates output named "output")
|
||||
const outputToRename = subgraph.outputs[0]
|
||||
subgraph.renameOutput(outputToRename, "new_name")
|
||||
|
||||
// Connection should persist and subgraph definition should be updated
|
||||
expect(externalNode.inputs[0].link).not.toBe(null)
|
||||
expect(subgraph.outputs[0].label).toBe("new_name")
|
||||
expect(subgraph.outputs[0].displayName).toBe("new_name")
|
||||
})
|
||||
// Connection should persist and subgraph definition should be updated
|
||||
expect(externalNode.inputs[0].link).not.toBe(null)
|
||||
expect(subgraph.outputs[0].label).toBe('new_name')
|
||||
expect(subgraph.outputs[0].displayName).toBe('new_name')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe("SubgraphIO - Boundary Connection Management", () => {
|
||||
subgraphTest("verifies cross-boundary link resolution", ({ complexSubgraph }) => {
|
||||
const subgraphNode = createTestSubgraphNode(complexSubgraph)
|
||||
const parentGraph = subgraphNode.graph!
|
||||
describe('SubgraphIO - Boundary Connection Management', () => {
|
||||
subgraphTest(
|
||||
'verifies cross-boundary link resolution',
|
||||
({ complexSubgraph }) => {
|
||||
const subgraphNode = createTestSubgraphNode(complexSubgraph)
|
||||
const parentGraph = subgraphNode.graph!
|
||||
|
||||
const externalSource = new LGraphNode("External Source")
|
||||
externalSource.addOutput("out", "number")
|
||||
parentGraph.add(externalSource)
|
||||
const externalSource = new LGraphNode('External Source')
|
||||
externalSource.addOutput('out', 'number')
|
||||
parentGraph.add(externalSource)
|
||||
|
||||
const externalTarget = new LGraphNode("External Target")
|
||||
externalTarget.addInput("in", "number")
|
||||
parentGraph.add(externalTarget)
|
||||
const externalTarget = new LGraphNode('External Target')
|
||||
externalTarget.addInput('in', 'number')
|
||||
parentGraph.add(externalTarget)
|
||||
|
||||
externalSource.connect(0, subgraphNode, 0)
|
||||
subgraphNode.connect(0, externalTarget, 0)
|
||||
externalSource.connect(0, subgraphNode, 0)
|
||||
subgraphNode.connect(0, externalTarget, 0)
|
||||
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
expect(externalTarget.inputs[0].link).not.toBe(null)
|
||||
})
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
expect(externalTarget.inputs[0].link).not.toBe(null)
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("handles bypass nodes that pass through data", ({ simpleSubgraph }) => {
|
||||
const subgraphNode = createTestSubgraphNode(simpleSubgraph)
|
||||
const parentGraph = subgraphNode.graph!
|
||||
subgraphTest(
|
||||
'handles bypass nodes that pass through data',
|
||||
({ simpleSubgraph }) => {
|
||||
const subgraphNode = createTestSubgraphNode(simpleSubgraph)
|
||||
const parentGraph = subgraphNode.graph!
|
||||
|
||||
const externalSource = new LGraphNode("External Source")
|
||||
externalSource.addOutput("out", "number")
|
||||
parentGraph.add(externalSource)
|
||||
const externalSource = new LGraphNode('External Source')
|
||||
externalSource.addOutput('out', 'number')
|
||||
parentGraph.add(externalSource)
|
||||
|
||||
const externalTarget = new LGraphNode("External Target")
|
||||
externalTarget.addInput("in", "number")
|
||||
parentGraph.add(externalTarget)
|
||||
const externalTarget = new LGraphNode('External Target')
|
||||
externalTarget.addInput('in', 'number')
|
||||
parentGraph.add(externalTarget)
|
||||
|
||||
externalSource.connect(0, subgraphNode, 0)
|
||||
subgraphNode.connect(0, externalTarget, 0)
|
||||
externalSource.connect(0, subgraphNode, 0)
|
||||
subgraphNode.connect(0, externalTarget, 0)
|
||||
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
expect(externalTarget.inputs[0].link).not.toBe(null)
|
||||
})
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
expect(externalTarget.inputs[0].link).not.toBe(null)
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("tests link integrity across subgraph boundaries", ({ subgraphWithNode }) => {
|
||||
const { subgraphNode, parentGraph } = subgraphWithNode
|
||||
subgraphTest(
|
||||
'tests link integrity across subgraph boundaries',
|
||||
({ subgraphWithNode }) => {
|
||||
const { subgraphNode, parentGraph } = subgraphWithNode
|
||||
|
||||
const externalSource = new LGraphNode("External Source")
|
||||
externalSource.addOutput("out", "*")
|
||||
parentGraph.add(externalSource)
|
||||
const externalSource = new LGraphNode('External Source')
|
||||
externalSource.addOutput('out', '*')
|
||||
parentGraph.add(externalSource)
|
||||
|
||||
const externalTarget = new LGraphNode("External Target")
|
||||
externalTarget.addInput("in", "*")
|
||||
parentGraph.add(externalTarget)
|
||||
const externalTarget = new LGraphNode('External Target')
|
||||
externalTarget.addInput('in', '*')
|
||||
parentGraph.add(externalTarget)
|
||||
|
||||
externalSource.connect(0, subgraphNode, 0)
|
||||
subgraphNode.connect(0, externalTarget, 0)
|
||||
externalSource.connect(0, subgraphNode, 0)
|
||||
subgraphNode.connect(0, externalTarget, 0)
|
||||
|
||||
const inputBoundaryLink = subgraphNode.inputs[0].link
|
||||
const outputBoundaryLink = externalTarget.inputs[0].link
|
||||
const inputBoundaryLink = subgraphNode.inputs[0].link
|
||||
const outputBoundaryLink = externalTarget.inputs[0].link
|
||||
|
||||
expect(inputBoundaryLink).toBeTruthy()
|
||||
expect(outputBoundaryLink).toBeTruthy()
|
||||
expect(inputBoundaryLink).toBeTruthy()
|
||||
expect(outputBoundaryLink).toBeTruthy()
|
||||
|
||||
// Links should exist in parent graph
|
||||
expect(inputBoundaryLink).toBeTruthy()
|
||||
expect(outputBoundaryLink).toBeTruthy()
|
||||
})
|
||||
// Links should exist in parent graph
|
||||
expect(inputBoundaryLink).toBeTruthy()
|
||||
expect(outputBoundaryLink).toBeTruthy()
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest("verifies proper link cleanup on slot removal", ({ complexSubgraph }) => {
|
||||
const subgraphNode = createTestSubgraphNode(complexSubgraph)
|
||||
const parentGraph = subgraphNode.graph!
|
||||
subgraphTest(
|
||||
'verifies proper link cleanup on slot removal',
|
||||
({ complexSubgraph }) => {
|
||||
const subgraphNode = createTestSubgraphNode(complexSubgraph)
|
||||
const parentGraph = subgraphNode.graph!
|
||||
|
||||
const externalSource = new LGraphNode("External Source")
|
||||
externalSource.addOutput("out", "number")
|
||||
parentGraph.add(externalSource)
|
||||
const externalSource = new LGraphNode('External Source')
|
||||
externalSource.addOutput('out', 'number')
|
||||
parentGraph.add(externalSource)
|
||||
|
||||
const externalTarget = new LGraphNode("External Target")
|
||||
externalTarget.addInput("in", "number")
|
||||
parentGraph.add(externalTarget)
|
||||
const externalTarget = new LGraphNode('External Target')
|
||||
externalTarget.addInput('in', 'number')
|
||||
parentGraph.add(externalTarget)
|
||||
|
||||
externalSource.connect(0, subgraphNode, 0)
|
||||
subgraphNode.connect(0, externalTarget, 0)
|
||||
externalSource.connect(0, subgraphNode, 0)
|
||||
subgraphNode.connect(0, externalTarget, 0)
|
||||
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
expect(externalTarget.inputs[0].link).not.toBe(null)
|
||||
expect(subgraphNode.inputs[0].link).not.toBe(null)
|
||||
expect(externalTarget.inputs[0].link).not.toBe(null)
|
||||
|
||||
const inputToRemove = complexSubgraph.inputs[0]
|
||||
complexSubgraph.removeInput(inputToRemove)
|
||||
const inputToRemove = complexSubgraph.inputs[0]
|
||||
complexSubgraph.removeInput(inputToRemove)
|
||||
|
||||
expect(subgraphNode.inputs.findIndex(i => i.name === "data")).toBe(-1)
|
||||
expect(externalSource.outputs[0].links).toHaveLength(0)
|
||||
expect(subgraphNode.inputs.findIndex((i) => i.name === 'data')).toBe(-1)
|
||||
expect(externalSource.outputs[0].links).toHaveLength(0)
|
||||
|
||||
const outputToRemove = complexSubgraph.outputs[0]
|
||||
complexSubgraph.removeOutput(outputToRemove)
|
||||
const outputToRemove = complexSubgraph.outputs[0]
|
||||
complexSubgraph.removeOutput(outputToRemove)
|
||||
|
||||
expect(subgraphNode.outputs.findIndex(o => o.name === "result")).toBe(-1)
|
||||
expect(externalTarget.inputs[0].link).toBe(null)
|
||||
})
|
||||
expect(subgraphNode.outputs.findIndex((o) => o.name === 'result')).toBe(
|
||||
-1
|
||||
)
|
||||
expect(externalTarget.inputs[0].link).toBe(null)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe("SubgraphIO - Advanced Scenarios", () => {
|
||||
it("handles multiple inputs and outputs with complex connections", () => {
|
||||
describe('SubgraphIO - Advanced Scenarios', () => {
|
||||
it('handles multiple inputs and outputs with complex connections', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Complex IO Test",
|
||||
name: 'Complex IO Test',
|
||||
inputs: [
|
||||
{ name: "input1", type: "number" },
|
||||
{ name: "input2", type: "string" },
|
||||
{ name: "input3", type: "boolean" },
|
||||
{ name: 'input1', type: 'number' },
|
||||
{ name: 'input2', type: 'string' },
|
||||
{ name: 'input3', type: 'boolean' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "output1", type: "number" },
|
||||
{ name: "output2", type: "string" },
|
||||
],
|
||||
{ name: 'output1', type: 'number' },
|
||||
{ name: 'output2', type: 'string' }
|
||||
]
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -282,16 +324,16 @@ describe("SubgraphIO - Advanced Scenarios", () => {
|
||||
expect(subgraphNode.outputs.length).toBe(2)
|
||||
|
||||
// Each slot should have correct type
|
||||
expect(subgraphNode.inputs[0].type).toBe("number")
|
||||
expect(subgraphNode.inputs[1].type).toBe("string")
|
||||
expect(subgraphNode.inputs[2].type).toBe("boolean")
|
||||
expect(subgraphNode.outputs[0].type).toBe("number")
|
||||
expect(subgraphNode.outputs[1].type).toBe("string")
|
||||
expect(subgraphNode.inputs[0].type).toBe('number')
|
||||
expect(subgraphNode.inputs[1].type).toBe('string')
|
||||
expect(subgraphNode.inputs[2].type).toBe('boolean')
|
||||
expect(subgraphNode.outputs[0].type).toBe('number')
|
||||
expect(subgraphNode.outputs[1].type).toBe('string')
|
||||
})
|
||||
|
||||
it("handles dynamic slot creation and removal", () => {
|
||||
it('handles dynamic slot creation and removal', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Dynamic IO Test",
|
||||
name: 'Dynamic IO Test'
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -301,14 +343,14 @@ describe("SubgraphIO - Advanced Scenarios", () => {
|
||||
expect(subgraphNode.outputs.length).toBe(0)
|
||||
|
||||
// Add slots dynamically
|
||||
subgraph.addInput("dynamic_input", "number")
|
||||
subgraph.addOutput("dynamic_output", "string")
|
||||
subgraph.addInput('dynamic_input', 'number')
|
||||
subgraph.addOutput('dynamic_output', 'string')
|
||||
|
||||
// SubgraphNode should automatically update
|
||||
expect(subgraphNode.inputs.length).toBe(1)
|
||||
expect(subgraphNode.outputs.length).toBe(1)
|
||||
expect(subgraphNode.inputs[0].name).toBe("dynamic_input")
|
||||
expect(subgraphNode.outputs[0].name).toBe("dynamic_output")
|
||||
expect(subgraphNode.inputs[0].name).toBe('dynamic_input')
|
||||
expect(subgraphNode.outputs[0].name).toBe('dynamic_output')
|
||||
|
||||
// Remove slots
|
||||
subgraph.removeInput(subgraph.inputs[0])
|
||||
@@ -319,11 +361,11 @@ describe("SubgraphIO - Advanced Scenarios", () => {
|
||||
expect(subgraphNode.outputs.length).toBe(0)
|
||||
})
|
||||
|
||||
it("maintains slot synchronization across multiple instances", () => {
|
||||
it('maintains slot synchronization across multiple instances', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Multi-Instance Test",
|
||||
inputs: [{ name: "shared_input", type: "number" }],
|
||||
outputs: [{ name: "shared_output", type: "number" }],
|
||||
name: 'Multi-Instance Test',
|
||||
inputs: [{ name: 'shared_input', type: 'number' }],
|
||||
outputs: [{ name: 'shared_output', type: 'number' }]
|
||||
})
|
||||
|
||||
// Create multiple instances
|
||||
@@ -337,8 +379,8 @@ describe("SubgraphIO - Advanced Scenarios", () => {
|
||||
expect(instance3.inputs.length).toBe(1)
|
||||
|
||||
// Modify the subgraph definition
|
||||
subgraph.addInput("new_input", "string")
|
||||
subgraph.addOutput("new_output", "boolean")
|
||||
subgraph.addInput('new_input', 'string')
|
||||
subgraph.addOutput('new_output', 'boolean')
|
||||
|
||||
// All instances should automatically update
|
||||
expect(instance1.inputs.length).toBe(2)
|
||||
@@ -350,37 +392,40 @@ describe("SubgraphIO - Advanced Scenarios", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphIO - Empty Slot Connection", () => {
|
||||
subgraphTest("creates new input and connects when dragging from empty slot inside subgraph", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode } = subgraphWithNode
|
||||
describe('SubgraphIO - Empty Slot Connection', () => {
|
||||
subgraphTest(
|
||||
'creates new input and connects when dragging from empty slot inside subgraph',
|
||||
({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode } = subgraphWithNode
|
||||
|
||||
// Create a node inside the subgraph that will receive the connection
|
||||
const internalNode = new LGraphNode("Internal Node")
|
||||
internalNode.addInput("in", "string")
|
||||
subgraph.add(internalNode)
|
||||
// Create a node inside the subgraph that will receive the connection
|
||||
const internalNode = new LGraphNode('Internal Node')
|
||||
internalNode.addInput('in', 'string')
|
||||
subgraph.add(internalNode)
|
||||
|
||||
// Simulate the connection process from the empty slot to an internal node
|
||||
// The -1 indicates a connection from the "empty" slot
|
||||
subgraph.inputNode.connectByType(-1, internalNode, "string")
|
||||
// Simulate the connection process from the empty slot to an internal node
|
||||
// The -1 indicates a connection from the "empty" slot
|
||||
subgraph.inputNode.connectByType(-1, internalNode, 'string')
|
||||
|
||||
// 1. A new input should have been created on the subgraph
|
||||
expect(subgraph.inputs.length).toBe(2) // Fixture adds one input already
|
||||
const newInput = subgraph.inputs[1]
|
||||
expect(newInput.name).toBe("in")
|
||||
expect(newInput.type).toBe("string")
|
||||
// 1. A new input should have been created on the subgraph
|
||||
expect(subgraph.inputs.length).toBe(2) // Fixture adds one input already
|
||||
const newInput = subgraph.inputs[1]
|
||||
expect(newInput.name).toBe('in')
|
||||
expect(newInput.type).toBe('string')
|
||||
|
||||
// 2. The subgraph node should now have a corresponding real input slot
|
||||
expect(subgraphNode.inputs.length).toBe(2)
|
||||
const subgraphInputSlot = subgraphNode.inputs[1]
|
||||
expect(subgraphInputSlot.name).toBe("in")
|
||||
// 2. The subgraph node should now have a corresponding real input slot
|
||||
expect(subgraphNode.inputs.length).toBe(2)
|
||||
const subgraphInputSlot = subgraphNode.inputs[1]
|
||||
expect(subgraphInputSlot.name).toBe('in')
|
||||
|
||||
// 3. A link should be established inside the subgraph
|
||||
expect(internalNode.inputs[0].link).not.toBe(null)
|
||||
const link = subgraph.links.get(internalNode.inputs[0].link!)
|
||||
expect(link).toBeDefined()
|
||||
expect(link.target_id).toBe(internalNode.id)
|
||||
expect(link.target_slot).toBe(0)
|
||||
expect(link.origin_id).toBe(subgraph.inputNode.id)
|
||||
expect(link.origin_slot).toBe(1) // Should be the second slot
|
||||
})
|
||||
// 3. A link should be established inside the subgraph
|
||||
expect(internalNode.inputs[0].link).not.toBe(null)
|
||||
const link = subgraph.links.get(internalNode.inputs[0].link!)
|
||||
expect(link).toBeDefined()
|
||||
expect(link.target_id).toBe(internalNode.id)
|
||||
expect(link.target_slot).toBe(0)
|
||||
expect(link.origin_id).toBe(subgraph.inputNode.id)
|
||||
expect(link.origin_slot).toBe(1) // Should be the second slot
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraph } from "@/lib/litegraph/src/litegraph"
|
||||
import { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
import { subgraphTest } from './fixtures/subgraphFixtures'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
} from "./fixtures/subgraphHelpers"
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("SubgraphNode Memory Management", () => {
|
||||
describe("Event Listener Cleanup", () => {
|
||||
it("should register event listeners on construction", () => {
|
||||
describe('SubgraphNode Memory Management', () => {
|
||||
describe('Event Listener Cleanup', () => {
|
||||
it('should register event listeners on construction', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// Spy on addEventListener to track listener registration
|
||||
const addEventSpy = vi.spyOn(subgraph.events, "addEventListener")
|
||||
const addEventSpy = vi.spyOn(subgraph.events, 'addEventListener')
|
||||
const initialCalls = addEventSpy.mock.calls.length
|
||||
|
||||
createTestSubgraphNode(subgraph)
|
||||
@@ -23,39 +23,43 @@ describe("SubgraphNode Memory Management", () => {
|
||||
expect(addEventSpy.mock.calls.length).toBeGreaterThan(initialCalls)
|
||||
|
||||
// Should have registered listeners for all major events
|
||||
const eventTypes = addEventSpy.mock.calls.map(call => call[0])
|
||||
expect(eventTypes).toContain("input-added")
|
||||
expect(eventTypes).toContain("removing-input")
|
||||
expect(eventTypes).toContain("output-added")
|
||||
expect(eventTypes).toContain("removing-output")
|
||||
expect(eventTypes).toContain("renaming-input")
|
||||
expect(eventTypes).toContain("renaming-output")
|
||||
const eventTypes = addEventSpy.mock.calls.map((call) => call[0])
|
||||
expect(eventTypes).toContain('input-added')
|
||||
expect(eventTypes).toContain('removing-input')
|
||||
expect(eventTypes).toContain('output-added')
|
||||
expect(eventTypes).toContain('removing-output')
|
||||
expect(eventTypes).toContain('renaming-input')
|
||||
expect(eventTypes).toContain('renaming-output')
|
||||
})
|
||||
|
||||
it("should clean up input listeners on removal", () => {
|
||||
it('should clean up input listeners on removal', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input1", type: "number" }],
|
||||
inputs: [{ name: 'input1', type: 'number' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Add input should have created listeners
|
||||
expect(subgraphNode.inputs[0]._listenerController).toBeDefined()
|
||||
expect(subgraphNode.inputs[0]._listenerController?.signal.aborted).toBe(false)
|
||||
expect(subgraphNode.inputs[0]._listenerController?.signal.aborted).toBe(
|
||||
false
|
||||
)
|
||||
|
||||
// Call onRemoved to simulate node removal
|
||||
subgraphNode.onRemoved()
|
||||
|
||||
// Input listeners should be aborted
|
||||
expect(subgraphNode.inputs[0]._listenerController?.signal.aborted).toBe(true)
|
||||
expect(subgraphNode.inputs[0]._listenerController?.signal.aborted).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("should not accumulate listeners during reconfiguration", () => {
|
||||
it('should not accumulate listeners during reconfiguration', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input1", type: "number" }],
|
||||
inputs: [{ name: 'input1', type: 'number' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
const addEventSpy = vi.spyOn(subgraph.events, "addEventListener")
|
||||
const addEventSpy = vi.spyOn(subgraph.events, 'addEventListener')
|
||||
const initialCalls = addEventSpy.mock.calls.length
|
||||
|
||||
// Reconfigure multiple times
|
||||
@@ -69,7 +73,7 @@ describe("SubgraphNode Memory Management", () => {
|
||||
outputs: [],
|
||||
properties: {},
|
||||
flags: {},
|
||||
mode: 0,
|
||||
mode: 0
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,32 +84,32 @@ describe("SubgraphNode Memory Management", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Widget Promotion Memory Management", () => {
|
||||
it("should clean up promoted widget references", () => {
|
||||
describe('Widget Promotion Memory Management', () => {
|
||||
it('should clean up promoted widget references', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "testInput", type: "number" }],
|
||||
inputs: [{ name: 'testInput', type: 'number' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Simulate widget promotion scenario
|
||||
const input = subgraphNode.inputs[0]
|
||||
const mockWidget = {
|
||||
type: "number",
|
||||
name: "promoted_widget",
|
||||
type: 'number',
|
||||
name: 'promoted_widget',
|
||||
value: 123,
|
||||
draw: vi.fn(),
|
||||
mouse: vi.fn(),
|
||||
computeSize: vi.fn(),
|
||||
createCopyForNode: vi.fn().mockReturnValue({
|
||||
type: "number",
|
||||
name: "promoted_widget",
|
||||
value: 123,
|
||||
}),
|
||||
type: 'number',
|
||||
name: 'promoted_widget',
|
||||
value: 123
|
||||
})
|
||||
}
|
||||
|
||||
// Simulate widget promotion
|
||||
input._widget = mockWidget
|
||||
input.widget = { name: "promoted_widget" }
|
||||
input.widget = { name: 'promoted_widget' }
|
||||
subgraphNode.widgets.push(mockWidget)
|
||||
|
||||
expect(input._widget).toBe(mockWidget)
|
||||
@@ -119,9 +123,9 @@ describe("SubgraphNode Memory Management", () => {
|
||||
expect(subgraphNode.widgets).not.toContain(mockWidget)
|
||||
})
|
||||
|
||||
it("should not leak widgets during reconfiguration", () => {
|
||||
it('should not leak widgets during reconfiguration', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input1", type: "number" }],
|
||||
inputs: [{ name: 'input1', type: 'number' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -139,7 +143,7 @@ describe("SubgraphNode Memory Management", () => {
|
||||
outputs: [],
|
||||
properties: {},
|
||||
flags: {},
|
||||
mode: 0,
|
||||
mode: 0
|
||||
})
|
||||
}
|
||||
|
||||
@@ -149,67 +153,35 @@ describe("SubgraphNode Memory Management", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphMemory - Event Listener Management", () => {
|
||||
subgraphTest("event handlers still work after node creation", ({ emptySubgraph }) => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraphNode = createTestSubgraphNode(emptySubgraph)
|
||||
rootGraph.add(subgraphNode)
|
||||
|
||||
const handler = vi.fn()
|
||||
emptySubgraph.events.addEventListener("input-added", handler)
|
||||
|
||||
emptySubgraph.addInput("test", "number")
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
expect(handler).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: "input-added",
|
||||
}))
|
||||
})
|
||||
|
||||
subgraphTest("can add and remove multiple nodes without errors", ({ emptySubgraph }) => {
|
||||
const rootGraph = new LGraph()
|
||||
const nodes: ReturnType<typeof createTestSubgraphNode>[] = []
|
||||
|
||||
// Should be able to create multiple nodes without issues
|
||||
for (let i = 0; i < 5; i++) {
|
||||
describe('SubgraphMemory - Event Listener Management', () => {
|
||||
subgraphTest(
|
||||
'event handlers still work after node creation',
|
||||
({ emptySubgraph }) => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraphNode = createTestSubgraphNode(emptySubgraph)
|
||||
rootGraph.add(subgraphNode)
|
||||
nodes.push(subgraphNode)
|
||||
|
||||
const handler = vi.fn()
|
||||
emptySubgraph.events.addEventListener('input-added', handler)
|
||||
|
||||
emptySubgraph.addInput('test', 'number')
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'input-added'
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
expect(rootGraph.nodes.length).toBe(5)
|
||||
|
||||
// Should be able to remove them all without issues
|
||||
for (const node of nodes) {
|
||||
rootGraph.remove(node)
|
||||
}
|
||||
|
||||
expect(rootGraph.nodes.length).toBe(0)
|
||||
})
|
||||
|
||||
subgraphTest("supports AbortController cleanup patterns", ({ emptySubgraph }) => {
|
||||
const abortController = new AbortController()
|
||||
const { signal } = abortController
|
||||
|
||||
const handler = vi.fn()
|
||||
|
||||
emptySubgraph.events.addEventListener("input-added", handler, { signal })
|
||||
|
||||
emptySubgraph.addInput("test1", "number")
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
|
||||
abortController.abort()
|
||||
|
||||
emptySubgraph.addInput("test2", "number")
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
subgraphTest("handles multiple creation/deletion cycles", ({ emptySubgraph }) => {
|
||||
const rootGraph = new LGraph()
|
||||
|
||||
for (let cycle = 0; cycle < 3; cycle++) {
|
||||
const nodes = []
|
||||
subgraphTest(
|
||||
'can add and remove multiple nodes without errors',
|
||||
({ emptySubgraph }) => {
|
||||
const rootGraph = new LGraph()
|
||||
const nodes: ReturnType<typeof createTestSubgraphNode>[] = []
|
||||
|
||||
// Should be able to create multiple nodes without issues
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const subgraphNode = createTestSubgraphNode(emptySubgraph)
|
||||
rootGraph.add(subgraphNode)
|
||||
@@ -218,17 +190,63 @@ describe("SubgraphMemory - Event Listener Management", () => {
|
||||
|
||||
expect(rootGraph.nodes.length).toBe(5)
|
||||
|
||||
// Should be able to remove them all without issues
|
||||
for (const node of nodes) {
|
||||
rootGraph.remove(node)
|
||||
}
|
||||
|
||||
expect(rootGraph.nodes.length).toBe(0)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
subgraphTest(
|
||||
'supports AbortController cleanup patterns',
|
||||
({ emptySubgraph }) => {
|
||||
const abortController = new AbortController()
|
||||
const { signal } = abortController
|
||||
|
||||
const handler = vi.fn()
|
||||
|
||||
emptySubgraph.events.addEventListener('input-added', handler, { signal })
|
||||
|
||||
emptySubgraph.addInput('test1', 'number')
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
|
||||
abortController.abort()
|
||||
|
||||
emptySubgraph.addInput('test2', 'number')
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
)
|
||||
|
||||
subgraphTest(
|
||||
'handles multiple creation/deletion cycles',
|
||||
({ emptySubgraph }) => {
|
||||
const rootGraph = new LGraph()
|
||||
|
||||
for (let cycle = 0; cycle < 3; cycle++) {
|
||||
const nodes = []
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const subgraphNode = createTestSubgraphNode(emptySubgraph)
|
||||
rootGraph.add(subgraphNode)
|
||||
nodes.push(subgraphNode)
|
||||
}
|
||||
|
||||
expect(rootGraph.nodes.length).toBe(5)
|
||||
|
||||
for (const node of nodes) {
|
||||
rootGraph.remove(node)
|
||||
}
|
||||
|
||||
expect(rootGraph.nodes.length).toBe(0)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe("SubgraphMemory - Reference Management", () => {
|
||||
it("properly manages subgraph references in root graph", () => {
|
||||
describe('SubgraphMemory - Reference Management', () => {
|
||||
it('properly manages subgraph references in root graph', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphId = subgraph.id
|
||||
@@ -243,7 +261,7 @@ describe("SubgraphMemory - Reference Management", () => {
|
||||
expect(rootGraph.subgraphs.has(subgraphId)).toBe(false)
|
||||
})
|
||||
|
||||
it("maintains proper parent-child references", () => {
|
||||
it('maintains proper parent-child references', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraph = createTestSubgraph({ nodeCount: 2 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -258,7 +276,7 @@ describe("SubgraphMemory - Reference Management", () => {
|
||||
expect(rootGraph.nodes).not.toContain(subgraphNode)
|
||||
})
|
||||
|
||||
it("prevents circular reference creation", () => {
|
||||
it('prevents circular reference creation', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 1 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -271,39 +289,42 @@ describe("SubgraphMemory - Reference Management", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphMemory - Widget Reference Management", () => {
|
||||
subgraphTest("properly sets and clears widget references", ({ simpleSubgraph }) => {
|
||||
const subgraphNode = createTestSubgraphNode(simpleSubgraph)
|
||||
const input = subgraphNode.inputs[0]
|
||||
describe('SubgraphMemory - Widget Reference Management', () => {
|
||||
subgraphTest(
|
||||
'properly sets and clears widget references',
|
||||
({ simpleSubgraph }) => {
|
||||
const subgraphNode = createTestSubgraphNode(simpleSubgraph)
|
||||
const input = subgraphNode.inputs[0]
|
||||
|
||||
// Mock widget for testing
|
||||
const mockWidget = {
|
||||
type: "number",
|
||||
value: 42,
|
||||
name: "test_widget",
|
||||
// Mock widget for testing
|
||||
const mockWidget = {
|
||||
type: 'number',
|
||||
value: 42,
|
||||
name: 'test_widget'
|
||||
}
|
||||
|
||||
// Set widget reference
|
||||
if (input && '_widget' in input) {
|
||||
;(input as any)._widget = mockWidget
|
||||
expect((input as any)._widget).toBe(mockWidget)
|
||||
}
|
||||
|
||||
// Clear widget reference
|
||||
if (input && '_widget' in input) {
|
||||
;(input as any)._widget = undefined
|
||||
expect((input as any)._widget).toBeUndefined()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Set widget reference
|
||||
if (input && "_widget" in input) {
|
||||
;(input as any)._widget = mockWidget
|
||||
expect((input as any)._widget).toBe(mockWidget)
|
||||
}
|
||||
|
||||
// Clear widget reference
|
||||
if (input && "_widget" in input) {
|
||||
;(input as any)._widget = undefined
|
||||
expect((input as any)._widget).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
subgraphTest("maintains widget count consistency", ({ simpleSubgraph }) => {
|
||||
subgraphTest('maintains widget count consistency', ({ simpleSubgraph }) => {
|
||||
const subgraphNode = createTestSubgraphNode(simpleSubgraph)
|
||||
|
||||
const initialWidgetCount = subgraphNode.widgets?.length || 0
|
||||
|
||||
// Add mock widgets
|
||||
const widget1 = { type: "number", value: 1, name: "widget1" }
|
||||
const widget2 = { type: "string", value: "test", name: "widget2" }
|
||||
const widget1 = { type: 'number', value: 1, name: 'widget1' }
|
||||
const widget2 = { type: 'string', value: 'test', name: 'widget2' }
|
||||
|
||||
if (subgraphNode.widgets) {
|
||||
subgraphNode.widgets.push(widget1, widget2)
|
||||
@@ -317,67 +338,73 @@ describe("SubgraphMemory - Widget Reference Management", () => {
|
||||
}
|
||||
})
|
||||
|
||||
subgraphTest("cleans up references during node removal", ({ simpleSubgraph }) => {
|
||||
const subgraphNode = createTestSubgraphNode(simpleSubgraph)
|
||||
const input = subgraphNode.inputs[0]
|
||||
const output = subgraphNode.outputs[0]
|
||||
subgraphTest(
|
||||
'cleans up references during node removal',
|
||||
({ simpleSubgraph }) => {
|
||||
const subgraphNode = createTestSubgraphNode(simpleSubgraph)
|
||||
const input = subgraphNode.inputs[0]
|
||||
const output = subgraphNode.outputs[0]
|
||||
|
||||
// Set up references that should be cleaned up
|
||||
const mockReferences = {
|
||||
widget: { type: "number", value: 42 },
|
||||
connection: { id: 1, type: "number" },
|
||||
listener: vi.fn(),
|
||||
// Set up references that should be cleaned up
|
||||
const mockReferences = {
|
||||
widget: { type: 'number', value: 42 },
|
||||
connection: { id: 1, type: 'number' },
|
||||
listener: vi.fn()
|
||||
}
|
||||
|
||||
// Set references
|
||||
if (input) {
|
||||
;(input as any)._widget = mockReferences.widget
|
||||
;(input as any)._connection = mockReferences.connection
|
||||
}
|
||||
if (output) {
|
||||
;(input as any)._connection = mockReferences.connection
|
||||
}
|
||||
|
||||
// Verify references are set
|
||||
expect((input as any)?._widget).toBe(mockReferences.widget)
|
||||
expect((input as any)?._connection).toBe(mockReferences.connection)
|
||||
|
||||
// Simulate proper cleanup (what onRemoved should do)
|
||||
subgraphNode.onRemoved()
|
||||
|
||||
// Input-specific listeners should be cleaned up (this works)
|
||||
if (input && '_listenerController' in input) {
|
||||
expect((input as any)._listenerController?.signal.aborted).toBe(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Set references
|
||||
if (input) {
|
||||
;(input as any)._widget = mockReferences.widget
|
||||
;(input as any)._connection = mockReferences.connection
|
||||
}
|
||||
if (output) {
|
||||
;(input as any)._connection = mockReferences.connection
|
||||
}
|
||||
|
||||
// Verify references are set
|
||||
expect((input as any)?._widget).toBe(mockReferences.widget)
|
||||
expect((input as any)?._connection).toBe(mockReferences.connection)
|
||||
|
||||
// Simulate proper cleanup (what onRemoved should do)
|
||||
subgraphNode.onRemoved()
|
||||
|
||||
// Input-specific listeners should be cleaned up (this works)
|
||||
if (input && "_listenerController" in input) {
|
||||
expect((input as any)._listenerController?.signal.aborted).toBe(true)
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe("SubgraphMemory - Performance and Scale", () => {
|
||||
subgraphTest("handles multiple subgraphs in same graph", ({ subgraphWithNode }) => {
|
||||
const { parentGraph } = subgraphWithNode
|
||||
const subgraphA = createTestSubgraph({ name: "Subgraph A" })
|
||||
const subgraphB = createTestSubgraph({ name: "Subgraph B" })
|
||||
describe('SubgraphMemory - Performance and Scale', () => {
|
||||
subgraphTest(
|
||||
'handles multiple subgraphs in same graph',
|
||||
({ subgraphWithNode }) => {
|
||||
const { parentGraph } = subgraphWithNode
|
||||
const subgraphA = createTestSubgraph({ name: 'Subgraph A' })
|
||||
const subgraphB = createTestSubgraph({ name: 'Subgraph B' })
|
||||
|
||||
const nodeA = createTestSubgraphNode(subgraphA)
|
||||
const nodeB = createTestSubgraphNode(subgraphB)
|
||||
const nodeA = createTestSubgraphNode(subgraphA)
|
||||
const nodeB = createTestSubgraphNode(subgraphB)
|
||||
|
||||
parentGraph.add(nodeA)
|
||||
parentGraph.add(nodeB)
|
||||
parentGraph.add(nodeA)
|
||||
parentGraph.add(nodeB)
|
||||
|
||||
expect(nodeA.graph).toBe(parentGraph)
|
||||
expect(nodeB.graph).toBe(parentGraph)
|
||||
expect(parentGraph.nodes.length).toBe(3) // Original + nodeA + nodeB
|
||||
expect(nodeA.graph).toBe(parentGraph)
|
||||
expect(nodeB.graph).toBe(parentGraph)
|
||||
expect(parentGraph.nodes.length).toBe(3) // Original + nodeA + nodeB
|
||||
|
||||
parentGraph.remove(nodeA)
|
||||
parentGraph.remove(nodeB)
|
||||
parentGraph.remove(nodeA)
|
||||
parentGraph.remove(nodeB)
|
||||
|
||||
expect(parentGraph.nodes.length).toBe(1) // Only the original subgraphNode remains
|
||||
})
|
||||
expect(parentGraph.nodes.length).toBe(1) // Only the original subgraphNode remains
|
||||
}
|
||||
)
|
||||
|
||||
it("handles many instances without issues", () => {
|
||||
it('handles many instances without issues', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "stress_input", type: "number" }],
|
||||
outputs: [{ name: "stress_output", type: "number" }],
|
||||
inputs: [{ name: 'stress_input', type: 'number' }],
|
||||
outputs: [{ name: 'stress_output', type: 'number' }]
|
||||
})
|
||||
|
||||
const rootGraph = new LGraph()
|
||||
@@ -401,7 +428,7 @@ describe("SubgraphMemory - Performance and Scale", () => {
|
||||
expect(rootGraph.nodes.length).toBe(0)
|
||||
})
|
||||
|
||||
it("maintains consistent behavior across multiple cycles", () => {
|
||||
it('maintains consistent behavior across multiple cycles', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const rootGraph = new LGraph()
|
||||
|
||||
|
||||
@@ -4,23 +4,22 @@
|
||||
* Tests for SubgraphNode instances including construction,
|
||||
* IO synchronization, and edge cases.
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { LGraph, Subgraph } from "@/lib/litegraph/src/litegraph"
|
||||
|
||||
import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
import { subgraphTest } from './fixtures/subgraphFixtures'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
} from "./fixtures/subgraphHelpers"
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("SubgraphNode Construction", () => {
|
||||
it("should create a SubgraphNode from a subgraph definition", () => {
|
||||
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" }],
|
||||
name: 'Test Definition',
|
||||
inputs: [{ name: 'input', type: 'number' }],
|
||||
outputs: [{ name: 'output', type: 'number' }]
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -29,19 +28,19 @@ describe("SubgraphNode Construction", () => {
|
||||
expect(subgraphNode.subgraph).toBe(subgraph)
|
||||
expect(subgraphNode.type).toBe(subgraph.id)
|
||||
expect(subgraphNode.isVirtualNode).toBe(true)
|
||||
expect(subgraphNode.displayType).toBe("Subgraph node")
|
||||
expect(subgraphNode.displayType).toBe('Subgraph node')
|
||||
})
|
||||
|
||||
it("should configure from instance data", () => {
|
||||
it('should configure from instance data', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "value", type: "number" }],
|
||||
outputs: [{ name: "result", type: "number" }],
|
||||
inputs: [{ name: 'value', type: 'number' }],
|
||||
outputs: [{ name: 'result', type: 'number' }]
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, {
|
||||
id: 42,
|
||||
pos: [300, 150],
|
||||
size: [180, 80],
|
||||
size: [180, 80]
|
||||
})
|
||||
|
||||
expect(subgraphNode.id).toBe(42)
|
||||
@@ -49,7 +48,7 @@ describe("SubgraphNode Construction", () => {
|
||||
expect(Array.from(subgraphNode.size)).toEqual([180, 80])
|
||||
})
|
||||
|
||||
it("should maintain reference to root graph", () => {
|
||||
it('should maintain reference to root graph', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const parentGraph = subgraphNode.graph
|
||||
@@ -57,62 +56,68 @@ describe("SubgraphNode Construction", () => {
|
||||
expect(subgraphNode.rootGraph).toBe(parentGraph.rootGraph)
|
||||
})
|
||||
|
||||
subgraphTest("should synchronize slots with subgraph definition", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode } = subgraphWithNode
|
||||
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)
|
||||
})
|
||||
// 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
|
||||
subgraphTest(
|
||||
'should update slots when subgraph definition changes',
|
||||
({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode } = subgraphWithNode
|
||||
|
||||
const initialInputCount = subgraphNode.inputs.length
|
||||
const initialInputCount = subgraphNode.inputs.length
|
||||
|
||||
// Add an input to the subgraph definition
|
||||
subgraph.addInput("new_input", "string")
|
||||
// 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)
|
||||
expect(subgraphNode.inputs.at(-1)?.name).toBe("new_input")
|
||||
expect(subgraphNode.inputs.at(-1)?.type).toBe("string")
|
||||
})
|
||||
// SubgraphNode should automatically update (this tests the event system)
|
||||
expect(subgraphNode.inputs).toHaveLength(initialInputCount + 1)
|
||||
expect(subgraphNode.inputs.at(-1)?.name).toBe('new_input')
|
||||
expect(subgraphNode.inputs.at(-1)?.type).toBe('string')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe("SubgraphNode Synchronization", () => {
|
||||
it("should sync input addition", () => {
|
||||
describe('SubgraphNode Synchronization', () => {
|
||||
it('should sync input addition', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
expect(subgraphNode.inputs).toHaveLength(0)
|
||||
|
||||
subgraph.addInput("value", "number")
|
||||
subgraph.addInput('value', 'number')
|
||||
|
||||
expect(subgraphNode.inputs).toHaveLength(1)
|
||||
expect(subgraphNode.inputs[0].name).toBe("value")
|
||||
expect(subgraphNode.inputs[0].type).toBe("number")
|
||||
expect(subgraphNode.inputs[0].name).toBe('value')
|
||||
expect(subgraphNode.inputs[0].type).toBe('number')
|
||||
})
|
||||
|
||||
it("should sync output addition", () => {
|
||||
it('should sync output addition', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
expect(subgraphNode.outputs).toHaveLength(0)
|
||||
|
||||
subgraph.addOutput("result", "string")
|
||||
subgraph.addOutput('result', 'string')
|
||||
|
||||
expect(subgraphNode.outputs).toHaveLength(1)
|
||||
expect(subgraphNode.outputs[0].name).toBe("result")
|
||||
expect(subgraphNode.outputs[0].type).toBe("string")
|
||||
expect(subgraphNode.outputs[0].name).toBe('result')
|
||||
expect(subgraphNode.outputs[0].type).toBe('string')
|
||||
})
|
||||
|
||||
it("should sync input removal", () => {
|
||||
it('should sync input removal', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: "input1", type: "number" },
|
||||
{ name: "input2", type: "string" },
|
||||
],
|
||||
{ name: 'input1', type: 'number' },
|
||||
{ name: 'input2', type: 'string' }
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -121,15 +126,15 @@ describe("SubgraphNode Synchronization", () => {
|
||||
subgraph.removeInput(subgraph.inputs[0])
|
||||
|
||||
expect(subgraphNode.inputs).toHaveLength(1)
|
||||
expect(subgraphNode.inputs[0].name).toBe("input2")
|
||||
expect(subgraphNode.inputs[0].name).toBe('input2')
|
||||
})
|
||||
|
||||
it("should sync output removal", () => {
|
||||
it('should sync output removal', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
outputs: [
|
||||
{ name: "output1", type: "number" },
|
||||
{ name: "output2", type: "string" },
|
||||
],
|
||||
{ name: 'output1', type: 'number' },
|
||||
{ name: 'output2', type: 'string' }
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -138,42 +143,42 @@ describe("SubgraphNode Synchronization", () => {
|
||||
subgraph.removeOutput(subgraph.outputs[0])
|
||||
|
||||
expect(subgraphNode.outputs).toHaveLength(1)
|
||||
expect(subgraphNode.outputs[0].name).toBe("output2")
|
||||
expect(subgraphNode.outputs[0].name).toBe('output2')
|
||||
})
|
||||
|
||||
it("should sync slot renaming", () => {
|
||||
it('should sync slot renaming', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "oldName", type: "number" }],
|
||||
outputs: [{ name: "oldOutput", type: "string" }],
|
||||
inputs: [{ name: 'oldName', type: 'number' }],
|
||||
outputs: [{ name: 'oldOutput', type: 'string' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Rename input
|
||||
subgraph.inputs[0].label = "newName"
|
||||
subgraph.events.dispatch("renaming-input", {
|
||||
subgraph.inputs[0].label = 'newName'
|
||||
subgraph.events.dispatch('renaming-input', {
|
||||
input: subgraph.inputs[0],
|
||||
index: 0,
|
||||
oldName: "oldName",
|
||||
newName: "newName",
|
||||
oldName: 'oldName',
|
||||
newName: 'newName'
|
||||
})
|
||||
|
||||
expect(subgraphNode.inputs[0].label).toBe("newName")
|
||||
expect(subgraphNode.inputs[0].label).toBe('newName')
|
||||
|
||||
// Rename output
|
||||
subgraph.outputs[0].label = "newOutput"
|
||||
subgraph.events.dispatch("renaming-output", {
|
||||
subgraph.outputs[0].label = 'newOutput'
|
||||
subgraph.events.dispatch('renaming-output', {
|
||||
output: subgraph.outputs[0],
|
||||
index: 0,
|
||||
oldName: "oldOutput",
|
||||
newName: "newOutput",
|
||||
oldName: 'oldOutput',
|
||||
newName: 'newOutput'
|
||||
})
|
||||
|
||||
expect(subgraphNode.outputs[0].label).toBe("newOutput")
|
||||
expect(subgraphNode.outputs[0].label).toBe('newOutput')
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphNode Lifecycle", () => {
|
||||
it("should initialize with empty widgets array", () => {
|
||||
describe('SubgraphNode Lifecycle', () => {
|
||||
it('should initialize with empty widgets array', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -181,10 +186,10 @@ describe("SubgraphNode Lifecycle", () => {
|
||||
expect(subgraphNode.widgets).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle reconfiguration", () => {
|
||||
it('should handle reconfiguration', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input1", type: "number" }],
|
||||
outputs: [{ name: "output1", type: "string" }],
|
||||
inputs: [{ name: 'input1', type: 'number' }],
|
||||
outputs: [{ name: 'output1', type: 'string' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -193,8 +198,8 @@ describe("SubgraphNode Lifecycle", () => {
|
||||
expect(subgraphNode.outputs).toHaveLength(1)
|
||||
|
||||
// Add more slots to subgraph
|
||||
subgraph.addInput("input2", "string")
|
||||
subgraph.addOutput("output2", "number")
|
||||
subgraph.addInput('input2', 'string')
|
||||
subgraph.addOutput('output2', 'number')
|
||||
|
||||
// Reconfigure
|
||||
subgraphNode.configure({
|
||||
@@ -206,7 +211,7 @@ describe("SubgraphNode Lifecycle", () => {
|
||||
outputs: [],
|
||||
properties: {},
|
||||
flags: {},
|
||||
mode: 0,
|
||||
mode: 0
|
||||
})
|
||||
|
||||
// Should reflect updated subgraph structure
|
||||
@@ -214,7 +219,7 @@ describe("SubgraphNode Lifecycle", () => {
|
||||
expect(subgraphNode.outputs).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should handle removal lifecycle", () => {
|
||||
it('should handle removal lifecycle', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const parentGraph = new LGraph()
|
||||
@@ -231,8 +236,8 @@ describe("SubgraphNode Lifecycle", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphNode Basic Functionality", () => {
|
||||
it("should identify as subgraph node", () => {
|
||||
describe('SubgraphNode Basic Functionality', () => {
|
||||
it('should identify as subgraph node', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -240,39 +245,39 @@ describe("SubgraphNode Basic Functionality", () => {
|
||||
expect(subgraphNode.isVirtualNode).toBe(true)
|
||||
})
|
||||
|
||||
it("should inherit input types correctly", () => {
|
||||
it('should inherit input types correctly', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: "numberInput", type: "number" },
|
||||
{ name: "stringInput", type: "string" },
|
||||
{ name: "anyInput", type: "*" },
|
||||
],
|
||||
{ name: 'numberInput', type: 'number' },
|
||||
{ name: 'stringInput', type: 'string' },
|
||||
{ name: 'anyInput', type: '*' }
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
expect(subgraphNode.inputs[0].type).toBe("number")
|
||||
expect(subgraphNode.inputs[1].type).toBe("string")
|
||||
expect(subgraphNode.inputs[2].type).toBe("*")
|
||||
expect(subgraphNode.inputs[0].type).toBe('number')
|
||||
expect(subgraphNode.inputs[1].type).toBe('string')
|
||||
expect(subgraphNode.inputs[2].type).toBe('*')
|
||||
})
|
||||
|
||||
it("should inherit output types correctly", () => {
|
||||
it('should inherit output types correctly', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
outputs: [
|
||||
{ name: "numberOutput", type: "number" },
|
||||
{ name: "stringOutput", type: "string" },
|
||||
{ name: "anyOutput", type: "*" },
|
||||
],
|
||||
{ name: 'numberOutput', type: 'number' },
|
||||
{ name: 'stringOutput', type: 'string' },
|
||||
{ name: 'anyOutput', type: '*' }
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
expect(subgraphNode.outputs[0].type).toBe("number")
|
||||
expect(subgraphNode.outputs[1].type).toBe("string")
|
||||
expect(subgraphNode.outputs[2].type).toBe("*")
|
||||
expect(subgraphNode.outputs[0].type).toBe('number')
|
||||
expect(subgraphNode.outputs[1].type).toBe('string')
|
||||
expect(subgraphNode.outputs[2].type).toBe('*')
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphNode Execution", () => {
|
||||
it("should flatten to ExecutableNodeDTOs", () => {
|
||||
describe('SubgraphNode Execution', () => {
|
||||
it('should flatten to ExecutableNodeDTOs', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 3 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -285,24 +290,26 @@ describe("SubgraphNode Execution", () => {
|
||||
expect(flattened[2].id).toMatch(/^1:\d+$/)
|
||||
})
|
||||
|
||||
it.skip("should handle nested subgraph execution", () => {
|
||||
it.skip('should handle nested subgraph execution', () => {
|
||||
// FIXME: Complex nested structure requires proper parent graph setup
|
||||
// Skip for now - similar issue to ExecutableNodeDTO nested test
|
||||
// Will implement proper nested execution test in edge cases file
|
||||
const childSubgraph = createTestSubgraph({
|
||||
name: "Child",
|
||||
nodeCount: 1,
|
||||
name: 'Child',
|
||||
nodeCount: 1
|
||||
})
|
||||
|
||||
const parentSubgraph = createTestSubgraph({
|
||||
name: "Parent",
|
||||
nodeCount: 1,
|
||||
name: 'Parent',
|
||||
nodeCount: 1
|
||||
})
|
||||
|
||||
const childSubgraphNode = createTestSubgraphNode(childSubgraph, { id: 42 })
|
||||
parentSubgraph.add(childSubgraphNode)
|
||||
|
||||
const parentSubgraphNode = createTestSubgraphNode(parentSubgraph, { id: 10 })
|
||||
const parentSubgraphNode = createTestSubgraphNode(parentSubgraph, {
|
||||
id: 10
|
||||
})
|
||||
|
||||
const executableNodes = new Map()
|
||||
const flattened = parentSubgraphNode.getInnerNodes(executableNodes)
|
||||
@@ -310,10 +317,10 @@ describe("SubgraphNode Execution", () => {
|
||||
expect(flattened.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should resolve cross-boundary input links", () => {
|
||||
it('should resolve cross-boundary input links', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input1", type: "number" }],
|
||||
nodeCount: 1,
|
||||
inputs: [{ name: 'input1', type: 'number' }],
|
||||
nodeCount: 1
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -323,20 +330,20 @@ describe("SubgraphNode Execution", () => {
|
||||
expect(Array.isArray(resolved)).toBe(true)
|
||||
})
|
||||
|
||||
it("should resolve cross-boundary output links", () => {
|
||||
it('should resolve cross-boundary output links', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
outputs: [{ name: "output1", type: "number" }],
|
||||
nodeCount: 1,
|
||||
outputs: [{ name: 'output1', type: 'number' }],
|
||||
nodeCount: 1
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
const resolved = subgraphNode.resolveSubgraphOutputLink(0)
|
||||
|
||||
// May be undefined if no internal connection exists
|
||||
expect(resolved === undefined || typeof resolved === "object").toBe(true)
|
||||
expect(resolved === undefined || typeof resolved === 'object').toBe(true)
|
||||
})
|
||||
|
||||
it("should prevent infinite recursion", () => {
|
||||
it('should prevent infinite recursion', () => {
|
||||
// Cycle detection properly prevents infinite recursion when a subgraph contains itself
|
||||
const subgraph = createTestSubgraph({ nodeCount: 1 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -347,15 +354,17 @@ describe("SubgraphNode Execution", () => {
|
||||
const executableNodes = new Map()
|
||||
expect(() => {
|
||||
subgraphNode.getInnerNodes(executableNodes)
|
||||
}).toThrow(/Circular reference detected.*infinite loop in the subgraph hierarchy/i)
|
||||
}).toThrow(
|
||||
/Circular reference detected.*infinite loop in the subgraph hierarchy/i
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle nested subgraph execution", () => {
|
||||
it('should handle nested subgraph execution', () => {
|
||||
// This test verifies that subgraph nodes can be properly executed
|
||||
// when they contain other nodes and produce correct output
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Nested Execution Test",
|
||||
nodeCount: 3,
|
||||
name: 'Nested Execution Test',
|
||||
nodeCount: 3
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -368,21 +377,21 @@ describe("SubgraphNode Execution", () => {
|
||||
|
||||
// Each DTO should have proper execution context
|
||||
for (const dto of flattened) {
|
||||
expect(dto).toHaveProperty("id")
|
||||
expect(dto).toHaveProperty("graph")
|
||||
expect(dto).toHaveProperty("inputs")
|
||||
expect(dto).toHaveProperty('id')
|
||||
expect(dto).toHaveProperty('graph')
|
||||
expect(dto).toHaveProperty('inputs')
|
||||
expect(dto.id).toMatch(/^\d+:\d+$/) // Path-based ID format
|
||||
}
|
||||
})
|
||||
|
||||
it("should resolve cross-boundary links", () => {
|
||||
it('should resolve cross-boundary links', () => {
|
||||
// This test verifies that links can cross subgraph boundaries
|
||||
// Currently this is a basic test - full cross-boundary linking
|
||||
// requires more complex setup with actual connected nodes
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "external_input", type: "number" }],
|
||||
outputs: [{ name: "external_output", type: "number" }],
|
||||
nodeCount: 2,
|
||||
inputs: [{ name: 'external_input', type: 'number' }],
|
||||
outputs: [{ name: 'external_output', type: 'number' }],
|
||||
nodeCount: 2
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -390,8 +399,8 @@ describe("SubgraphNode Execution", () => {
|
||||
// Verify the subgraph node has the expected I/O structure for cross-boundary links
|
||||
expect(subgraphNode.inputs).toHaveLength(1)
|
||||
expect(subgraphNode.outputs).toHaveLength(1)
|
||||
expect(subgraphNode.inputs[0].name).toBe("external_input")
|
||||
expect(subgraphNode.outputs[0].name).toBe("external_output")
|
||||
expect(subgraphNode.inputs[0].name).toBe('external_input')
|
||||
expect(subgraphNode.outputs[0].name).toBe('external_output')
|
||||
|
||||
// Internal nodes should be flattened correctly
|
||||
const executableNodes = new Map()
|
||||
@@ -400,12 +409,12 @@ describe("SubgraphNode Execution", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphNode Edge Cases", () => {
|
||||
it("should handle deep nesting", () => {
|
||||
describe('SubgraphNode Edge Cases', () => {
|
||||
it('should handle deep nesting', () => {
|
||||
// Create a simpler deep nesting test that works with current implementation
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Deep Test",
|
||||
nodeCount: 5, // Multiple nodes to test flattening at depth
|
||||
name: 'Deep Test',
|
||||
nodeCount: 5 // Multiple nodes to test flattening at depth
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -425,7 +434,7 @@ describe("SubgraphNode Edge Cases", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("should validate against MAX_NESTED_SUBGRAPHS", () => {
|
||||
it('should validate against MAX_NESTED_SUBGRAPHS', () => {
|
||||
// Test that the MAX_NESTED_SUBGRAPHS constant exists
|
||||
// Note: Currently not enforced in the implementation
|
||||
expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBe(1000)
|
||||
@@ -435,8 +444,8 @@ describe("SubgraphNode Edge Cases", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphNode Integration", () => {
|
||||
it("should be addable to a parent graph", () => {
|
||||
describe('SubgraphNode Integration', () => {
|
||||
it('should be addable to a parent graph', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const parentGraph = new LGraph()
|
||||
@@ -447,15 +456,18 @@ describe("SubgraphNode Integration", () => {
|
||||
expect(subgraphNode.graph).toBe(parentGraph)
|
||||
})
|
||||
|
||||
subgraphTest("should maintain reference to root graph", ({ subgraphWithNode }) => {
|
||||
const { subgraphNode } = subgraphWithNode
|
||||
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()
|
||||
})
|
||||
// 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()
|
||||
}
|
||||
)
|
||||
|
||||
it("should handle graph removal properly", () => {
|
||||
it('should handle graph removal properly', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const parentGraph = new LGraph()
|
||||
@@ -468,33 +480,36 @@ describe("SubgraphNode Integration", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Foundation Test Utilities", () => {
|
||||
it("should create test SubgraphNodes with custom options", () => {
|
||||
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,
|
||||
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
|
||||
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)
|
||||
})
|
||||
expect(subgraph).toBeDefined()
|
||||
expect(subgraphNode).toBeDefined()
|
||||
expect(parentGraph).toBeDefined()
|
||||
expect(parentGraph.nodes).toContain(subgraphNode)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe("SubgraphNode Cleanup", () => {
|
||||
it("should clean up event listeners when removed", () => {
|
||||
describe('SubgraphNode Cleanup', () => {
|
||||
it('should clean up event listeners when removed', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
@@ -512,8 +527,8 @@ describe("SubgraphNode Cleanup", () => {
|
||||
rootGraph.remove(node2)
|
||||
|
||||
// Now trigger an event - only node1 should respond
|
||||
subgraph.events.dispatch("input-added", {
|
||||
input: { name: "test", type: "number", id: "test-id" } as any,
|
||||
subgraph.events.dispatch('input-added', {
|
||||
input: { name: 'test', type: 'number', id: 'test-id' } as any
|
||||
})
|
||||
|
||||
// Only node1 should have added an input
|
||||
@@ -521,7 +536,7 @@ describe("SubgraphNode Cleanup", () => {
|
||||
expect(node2.inputs.length).toBe(0) // node2 should NOT respond (but currently does)
|
||||
})
|
||||
|
||||
it("should not accumulate handlers over multiple add/remove cycles", () => {
|
||||
it('should not accumulate handlers over multiple add/remove cycles', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
@@ -540,8 +555,8 @@ describe("SubgraphNode Cleanup", () => {
|
||||
}
|
||||
|
||||
// Trigger an event - no nodes should respond
|
||||
subgraph.events.dispatch("input-added", {
|
||||
input: { name: "test", type: "number", id: "test-id" } as any,
|
||||
subgraph.events.dispatch('input-added', {
|
||||
input: { name: 'test', type: 'number', id: 'test-id' } as any
|
||||
})
|
||||
|
||||
// Without cleanup: all 3 removed nodes would have added an input
|
||||
@@ -551,10 +566,13 @@ describe("SubgraphNode Cleanup", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("should clean up input listener controllers on removal", () => {
|
||||
it('should clean up input listener controllers on removal', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "in1", type: "number" }, { name: "in2", type: "string" }],
|
||||
inputs: [
|
||||
{ name: 'in1', type: 'number' },
|
||||
{ name: 'in2', type: 'string' }
|
||||
]
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -565,8 +583,14 @@ describe("SubgraphNode Cleanup", () => {
|
||||
expect(subgraphNode.inputs[1]._listenerController).toBeDefined()
|
||||
|
||||
// Track abort calls
|
||||
const abortSpy1 = vi.spyOn(subgraphNode.inputs[0]._listenerController!, "abort")
|
||||
const abortSpy2 = vi.spyOn(subgraphNode.inputs[1]._listenerController!, "abort")
|
||||
const abortSpy1 = vi.spyOn(
|
||||
subgraphNode.inputs[0]._listenerController!,
|
||||
'abort'
|
||||
)
|
||||
const abortSpy2 = vi.spyOn(
|
||||
subgraphNode.inputs[1]._listenerController!,
|
||||
'abort'
|
||||
)
|
||||
|
||||
// Remove node
|
||||
rootGraph.remove(subgraphNode)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphButton } from "@/lib/litegraph/src/LGraphButton"
|
||||
import { LGraphCanvas } from "@/lib/litegraph/src/LGraphCanvas"
|
||||
import { LGraphButton } from '@/lib/litegraph/src/LGraphButton'
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
|
||||
import { createTestSubgraph, createTestSubgraphNode } from "./fixtures/subgraphHelpers"
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("SubgraphNode Title Button", () => {
|
||||
describe("Constructor", () => {
|
||||
it("should automatically add enter_subgraph button", () => {
|
||||
describe('SubgraphNode Title Button', () => {
|
||||
describe('Constructor', () => {
|
||||
it('should automatically add enter_subgraph button', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Test Subgraph",
|
||||
inputs: [{ name: "input", type: "number" }],
|
||||
name: 'Test Subgraph',
|
||||
inputs: [{ name: 'input', type: 'number' }]
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -19,33 +22,33 @@ describe("SubgraphNode Title Button", () => {
|
||||
|
||||
const button = subgraphNode.title_buttons[0]
|
||||
expect(button).toBeInstanceOf(LGraphButton)
|
||||
expect(button.name).toBe("enter_subgraph")
|
||||
expect(button.text).toBe("\uE93B") // pi-window-maximize
|
||||
expect(button.name).toBe('enter_subgraph')
|
||||
expect(button.text).toBe('\uE93B') // pi-window-maximize
|
||||
expect(button.xOffset).toBe(-10)
|
||||
expect(button.yOffset).toBe(0)
|
||||
expect(button.fontSize).toBe(16)
|
||||
})
|
||||
|
||||
it("should preserve enter_subgraph button when adding more buttons", () => {
|
||||
it('should preserve enter_subgraph button when adding more buttons', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Add another button
|
||||
const customButton = subgraphNode.addTitleButton({
|
||||
name: "custom_button",
|
||||
text: "C",
|
||||
name: 'custom_button',
|
||||
text: 'C'
|
||||
})
|
||||
|
||||
expect(subgraphNode.title_buttons).toHaveLength(2)
|
||||
expect(subgraphNode.title_buttons[0].name).toBe("enter_subgraph")
|
||||
expect(subgraphNode.title_buttons[0].name).toBe('enter_subgraph')
|
||||
expect(subgraphNode.title_buttons[1]).toBe(customButton)
|
||||
})
|
||||
})
|
||||
|
||||
describe("onTitleButtonClick", () => {
|
||||
it("should open subgraph when enter_subgraph button is clicked", () => {
|
||||
describe('onTitleButtonClick', () => {
|
||||
it('should open subgraph when enter_subgraph button is clicked', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Test Subgraph",
|
||||
name: 'Test Subgraph'
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -53,7 +56,7 @@ describe("SubgraphNode Title Button", () => {
|
||||
|
||||
const canvas = {
|
||||
openSubgraph: vi.fn(),
|
||||
dispatch: vi.fn(),
|
||||
dispatch: vi.fn()
|
||||
} as unknown as LGraphCanvas
|
||||
|
||||
subgraphNode.onTitleButtonClick(enterButton, canvas)
|
||||
@@ -62,35 +65,38 @@ describe("SubgraphNode Title Button", () => {
|
||||
expect(canvas.dispatch).not.toHaveBeenCalled() // Should not call parent implementation
|
||||
})
|
||||
|
||||
it("should call parent implementation for other buttons", () => {
|
||||
it('should call parent implementation for other buttons', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
const customButton = subgraphNode.addTitleButton({
|
||||
name: "custom_button",
|
||||
text: "X",
|
||||
name: 'custom_button',
|
||||
text: 'X'
|
||||
})
|
||||
|
||||
const canvas = {
|
||||
openSubgraph: vi.fn(),
|
||||
dispatch: vi.fn(),
|
||||
dispatch: vi.fn()
|
||||
} as unknown as LGraphCanvas
|
||||
|
||||
subgraphNode.onTitleButtonClick(customButton, canvas)
|
||||
|
||||
expect(canvas.openSubgraph).not.toHaveBeenCalled()
|
||||
expect(canvas.dispatch).toHaveBeenCalledWith("litegraph:node-title-button-clicked", {
|
||||
node: subgraphNode,
|
||||
button: customButton,
|
||||
})
|
||||
expect(canvas.dispatch).toHaveBeenCalledWith(
|
||||
'litegraph:node-title-button-clicked',
|
||||
{
|
||||
node: subgraphNode,
|
||||
button: customButton
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Integration with node click handling", () => {
|
||||
it("should handle clicks on enter_subgraph button", () => {
|
||||
describe('Integration with node click handling', () => {
|
||||
it('should handle clicks on enter_subgraph button', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Nested Subgraph",
|
||||
nodeCount: 3,
|
||||
name: 'Nested Subgraph',
|
||||
nodeCount: 3
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -111,31 +117,35 @@ describe("SubgraphNode Title Button", () => {
|
||||
|
||||
const canvas = {
|
||||
ctx: {
|
||||
measureText: vi.fn().mockReturnValue({ width: 25 }),
|
||||
measureText: vi.fn().mockReturnValue({ width: 25 })
|
||||
} as unknown as CanvasRenderingContext2D,
|
||||
openSubgraph: vi.fn(),
|
||||
dispatch: vi.fn(),
|
||||
dispatch: vi.fn()
|
||||
} as unknown as LGraphCanvas
|
||||
|
||||
// Simulate click on the enter button
|
||||
const event = {
|
||||
canvasX: 275, // Near right edge where button should be
|
||||
canvasY: 80, // In title area
|
||||
canvasY: 80 // In title area
|
||||
} as any
|
||||
|
||||
// Calculate node-relative position
|
||||
const clickPosRelativeToNode: [number, number] = [
|
||||
275 - subgraphNode.pos[0], // 275 - 100 = 175
|
||||
80 - subgraphNode.pos[1], // 80 - 100 = -20
|
||||
80 - subgraphNode.pos[1] // 80 - 100 = -20
|
||||
]
|
||||
|
||||
const handled = subgraphNode.onMouseDown(event, clickPosRelativeToNode, canvas)
|
||||
const handled = subgraphNode.onMouseDown(
|
||||
event,
|
||||
clickPosRelativeToNode,
|
||||
canvas
|
||||
)
|
||||
|
||||
expect(handled).toBe(true)
|
||||
expect(canvas.openSubgraph).toHaveBeenCalledWith(subgraph)
|
||||
})
|
||||
|
||||
it("should not interfere with normal node operations", () => {
|
||||
it('should not interfere with normal node operations', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
subgraphNode.pos = [100, 100]
|
||||
@@ -143,31 +153,35 @@ describe("SubgraphNode Title Button", () => {
|
||||
|
||||
const canvas = {
|
||||
ctx: {
|
||||
measureText: vi.fn().mockReturnValue({ width: 25 }),
|
||||
measureText: vi.fn().mockReturnValue({ width: 25 })
|
||||
} as unknown as CanvasRenderingContext2D,
|
||||
openSubgraph: vi.fn(),
|
||||
dispatch: vi.fn(),
|
||||
dispatch: vi.fn()
|
||||
} as unknown as LGraphCanvas
|
||||
|
||||
// Click in the body of the node, not on button
|
||||
const event = {
|
||||
canvasX: 200, // Middle of node
|
||||
canvasY: 150, // Body area
|
||||
canvasY: 150 // Body area
|
||||
} as any
|
||||
|
||||
// Calculate node-relative position
|
||||
const clickPosRelativeToNode: [number, number] = [
|
||||
200 - subgraphNode.pos[0], // 200 - 100 = 100
|
||||
150 - subgraphNode.pos[1], // 150 - 100 = 50
|
||||
150 - subgraphNode.pos[1] // 150 - 100 = 50
|
||||
]
|
||||
|
||||
const handled = subgraphNode.onMouseDown(event, clickPosRelativeToNode, canvas)
|
||||
const handled = subgraphNode.onMouseDown(
|
||||
event,
|
||||
clickPosRelativeToNode,
|
||||
canvas
|
||||
)
|
||||
|
||||
expect(handled).toBe(false)
|
||||
expect(canvas.openSubgraph).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not process button clicks when node is collapsed", () => {
|
||||
it('should not process button clicks when node is collapsed', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
subgraphNode.pos = [100, 100]
|
||||
@@ -186,24 +200,28 @@ describe("SubgraphNode Title Button", () => {
|
||||
|
||||
const canvas = {
|
||||
ctx: {
|
||||
measureText: vi.fn().mockReturnValue({ width: 25 }),
|
||||
measureText: vi.fn().mockReturnValue({ width: 25 })
|
||||
} as unknown as CanvasRenderingContext2D,
|
||||
openSubgraph: vi.fn(),
|
||||
dispatch: vi.fn(),
|
||||
dispatch: vi.fn()
|
||||
} as unknown as LGraphCanvas
|
||||
|
||||
// Try to click on where the button would be
|
||||
const event = {
|
||||
canvasX: 275,
|
||||
canvasY: 80,
|
||||
canvasY: 80
|
||||
} as any
|
||||
|
||||
const clickPosRelativeToNode: [number, number] = [
|
||||
275 - subgraphNode.pos[0], // 175
|
||||
80 - subgraphNode.pos[1], // -20
|
||||
80 - subgraphNode.pos[1] // -20
|
||||
]
|
||||
|
||||
const handled = subgraphNode.onMouseDown(event, clickPosRelativeToNode, canvas)
|
||||
const handled = subgraphNode.onMouseDown(
|
||||
event,
|
||||
clickPosRelativeToNode,
|
||||
canvas
|
||||
)
|
||||
|
||||
// Should not handle the click when collapsed
|
||||
expect(handled).toBe(false)
|
||||
@@ -211,15 +229,15 @@ describe("SubgraphNode Title Button", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Visual properties", () => {
|
||||
it("should have appropriate visual properties for enter button", () => {
|
||||
describe('Visual properties', () => {
|
||||
it('should have appropriate visual properties for enter button', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
const enterButton = subgraphNode.title_buttons[0]
|
||||
|
||||
// Check visual properties
|
||||
expect(enterButton.text).toBe("\uE93B") // pi-window-maximize
|
||||
expect(enterButton.text).toBe('\uE93B') // pi-window-maximize
|
||||
expect(enterButton.fontSize).toBe(16) // Icon size
|
||||
expect(enterButton.xOffset).toBe(-10) // Positioned from right edge
|
||||
expect(enterButton.yOffset).toBe(0) // Centered vertically
|
||||
|
||||
@@ -4,37 +4,36 @@
|
||||
* Tests for saving, loading, and version compatibility of subgraphs.
|
||||
* This covers serialization, deserialization, data integrity, and migration scenarios.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { LGraph, Subgraph } from "@/lib/litegraph/src/litegraph"
|
||||
import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
} from "./fixtures/subgraphHelpers"
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("SubgraphSerialization - Basic Serialization", () => {
|
||||
it("should save and load simple subgraphs", () => {
|
||||
describe('SubgraphSerialization - Basic Serialization', () => {
|
||||
it('should save and load simple subgraphs', () => {
|
||||
const original = createTestSubgraph({
|
||||
name: "Simple Test",
|
||||
nodeCount: 2,
|
||||
name: 'Simple Test',
|
||||
nodeCount: 2
|
||||
})
|
||||
original.addInput("in1", "number")
|
||||
original.addInput("in2", "string")
|
||||
original.addOutput("out", "boolean")
|
||||
original.addInput('in1', 'number')
|
||||
original.addInput('in2', 'string')
|
||||
original.addOutput('out', 'boolean')
|
||||
|
||||
// Serialize
|
||||
const exported = original.asSerialisable()
|
||||
|
||||
// Verify exported structure
|
||||
expect(exported).toHaveProperty("id", original.id)
|
||||
expect(exported).toHaveProperty("name", "Simple Test")
|
||||
expect(exported).toHaveProperty("nodes")
|
||||
expect(exported).toHaveProperty("links")
|
||||
expect(exported).toHaveProperty("inputs")
|
||||
expect(exported).toHaveProperty("outputs")
|
||||
expect(exported).toHaveProperty("version")
|
||||
expect(exported).toHaveProperty('id', original.id)
|
||||
expect(exported).toHaveProperty('name', 'Simple Test')
|
||||
expect(exported).toHaveProperty('nodes')
|
||||
expect(exported).toHaveProperty('links')
|
||||
expect(exported).toHaveProperty('inputs')
|
||||
expect(exported).toHaveProperty('outputs')
|
||||
expect(exported).toHaveProperty('version')
|
||||
|
||||
// Create new instance from serialized data
|
||||
const restored = new Subgraph(new LGraph(), exported)
|
||||
@@ -48,26 +47,26 @@ describe("SubgraphSerialization - Basic Serialization", () => {
|
||||
// This is expected behavior - serialization preserves I/O but nodes need valid types
|
||||
|
||||
// Verify input details
|
||||
expect(restored.inputs[0].name).toBe("in1")
|
||||
expect(restored.inputs[0].type).toBe("number")
|
||||
expect(restored.inputs[1].name).toBe("in2")
|
||||
expect(restored.inputs[1].type).toBe("string")
|
||||
expect(restored.outputs[0].name).toBe("out")
|
||||
expect(restored.outputs[0].type).toBe("boolean")
|
||||
expect(restored.inputs[0].name).toBe('in1')
|
||||
expect(restored.inputs[0].type).toBe('number')
|
||||
expect(restored.inputs[1].name).toBe('in2')
|
||||
expect(restored.inputs[1].type).toBe('string')
|
||||
expect(restored.outputs[0].name).toBe('out')
|
||||
expect(restored.outputs[0].type).toBe('boolean')
|
||||
})
|
||||
|
||||
it("should verify all properties are preserved", () => {
|
||||
it('should verify all properties are preserved', () => {
|
||||
const original = createTestSubgraph({
|
||||
name: "Property Test",
|
||||
name: 'Property Test',
|
||||
nodeCount: 3,
|
||||
inputs: [
|
||||
{ name: "input1", type: "number" },
|
||||
{ name: "input2", type: "string" },
|
||||
{ name: 'input1', type: 'number' },
|
||||
{ name: 'input2', type: 'string' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "output1", type: "boolean" },
|
||||
{ name: "output2", type: "array" },
|
||||
],
|
||||
{ name: 'output1', type: 'boolean' },
|
||||
{ name: 'output2', type: 'array' }
|
||||
]
|
||||
})
|
||||
|
||||
const exported = original.asSerialisable()
|
||||
@@ -95,18 +94,18 @@ describe("SubgraphSerialization - Basic Serialization", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("should test export() and configure() methods", () => {
|
||||
it('should test export() and configure() methods', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 1 })
|
||||
subgraph.addInput("test_input", "number")
|
||||
subgraph.addOutput("test_output", "string")
|
||||
subgraph.addInput('test_input', 'number')
|
||||
subgraph.addOutput('test_output', 'string')
|
||||
|
||||
// Test export
|
||||
const exported = subgraph.asSerialisable()
|
||||
expect(exported).toHaveProperty("id")
|
||||
expect(exported).toHaveProperty("nodes")
|
||||
expect(exported).toHaveProperty("links")
|
||||
expect(exported).toHaveProperty("inputs")
|
||||
expect(exported).toHaveProperty("outputs")
|
||||
expect(exported).toHaveProperty('id')
|
||||
expect(exported).toHaveProperty('nodes')
|
||||
expect(exported).toHaveProperty('links')
|
||||
expect(exported).toHaveProperty('inputs')
|
||||
expect(exported).toHaveProperty('outputs')
|
||||
|
||||
// Test configure with partial data
|
||||
const newSubgraph = createTestSubgraph({ nodeCount: 0 })
|
||||
@@ -117,26 +116,26 @@ describe("SubgraphSerialization - Basic Serialization", () => {
|
||||
// Verify configuration applied
|
||||
expect(newSubgraph.inputs.length).toBe(1)
|
||||
expect(newSubgraph.outputs.length).toBe(1)
|
||||
expect(newSubgraph.inputs[0].name).toBe("test_input")
|
||||
expect(newSubgraph.outputs[0].name).toBe("test_output")
|
||||
expect(newSubgraph.inputs[0].name).toBe('test_input')
|
||||
expect(newSubgraph.outputs[0].name).toBe('test_output')
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphSerialization - Complex Serialization", () => {
|
||||
it("should serialize nested subgraphs with multiple levels", () => {
|
||||
describe('SubgraphSerialization - Complex Serialization', () => {
|
||||
it('should serialize nested subgraphs with multiple levels', () => {
|
||||
// Create a nested structure
|
||||
const childSubgraph = createTestSubgraph({
|
||||
name: "Child",
|
||||
name: 'Child',
|
||||
nodeCount: 2,
|
||||
inputs: [{ name: "child_in", type: "number" }],
|
||||
outputs: [{ name: "child_out", type: "string" }],
|
||||
inputs: [{ name: 'child_in', type: 'number' }],
|
||||
outputs: [{ name: 'child_out', type: 'string' }]
|
||||
})
|
||||
|
||||
const parentSubgraph = createTestSubgraph({
|
||||
name: "Parent",
|
||||
name: 'Parent',
|
||||
nodeCount: 1,
|
||||
inputs: [{ name: "parent_in", type: "boolean" }],
|
||||
outputs: [{ name: "parent_out", type: "array" }],
|
||||
inputs: [{ name: 'parent_in', type: 'boolean' }],
|
||||
outputs: [{ name: 'parent_out', type: 'array' }]
|
||||
})
|
||||
|
||||
// Add child to parent
|
||||
@@ -148,30 +147,30 @@ describe("SubgraphSerialization - Complex Serialization", () => {
|
||||
const parentExported = parentSubgraph.asSerialisable()
|
||||
|
||||
// Verify both can be serialized
|
||||
expect(childExported).toHaveProperty("name", "Child")
|
||||
expect(parentExported).toHaveProperty("name", "Parent")
|
||||
expect(childExported).toHaveProperty('name', 'Child')
|
||||
expect(parentExported).toHaveProperty('name', 'Parent')
|
||||
expect(parentExported.nodes.length).toBe(2) // 1 original + 1 child subgraph
|
||||
|
||||
// Restore and verify
|
||||
const restoredChild = new Subgraph(new LGraph(), childExported)
|
||||
const restoredParent = new Subgraph(new LGraph(), parentExported)
|
||||
|
||||
expect(restoredChild.name).toBe("Child")
|
||||
expect(restoredParent.name).toBe("Parent")
|
||||
expect(restoredChild.name).toBe('Child')
|
||||
expect(restoredParent.name).toBe('Parent')
|
||||
expect(restoredChild.inputs.length).toBe(1)
|
||||
expect(restoredParent.inputs.length).toBe(1)
|
||||
})
|
||||
|
||||
it("should serialize subgraphs with many nodes and connections", () => {
|
||||
it('should serialize subgraphs with many nodes and connections', () => {
|
||||
const largeSubgraph = createTestSubgraph({
|
||||
name: "Large Subgraph",
|
||||
nodeCount: 10, // Many nodes
|
||||
name: 'Large Subgraph',
|
||||
nodeCount: 10 // Many nodes
|
||||
})
|
||||
|
||||
// Add many I/O slots
|
||||
for (let i = 0; i < 5; i++) {
|
||||
largeSubgraph.addInput(`input_${i}`, "number")
|
||||
largeSubgraph.addOutput(`output_${i}`, "string")
|
||||
largeSubgraph.addInput(`input_${i}`, 'number')
|
||||
largeSubgraph.addOutput(`output_${i}`, 'string')
|
||||
}
|
||||
|
||||
const exported = largeSubgraph.asSerialisable()
|
||||
@@ -189,7 +188,7 @@ describe("SubgraphSerialization - Complex Serialization", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("should preserve custom node data", () => {
|
||||
it('should preserve custom node data', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 2 })
|
||||
|
||||
// Add custom properties to nodes (if supported)
|
||||
@@ -198,7 +197,7 @@ describe("SubgraphSerialization - Complex Serialization", () => {
|
||||
const firstNode = nodes[0]
|
||||
if (firstNode.properties) {
|
||||
firstNode.properties.customValue = 42
|
||||
firstNode.properties.customString = "test"
|
||||
firstNode.properties.customString = 'test'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,52 +216,52 @@ describe("SubgraphSerialization - Complex Serialization", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphSerialization - Version Compatibility", () => {
|
||||
it("should handle version field in exports", () => {
|
||||
describe('SubgraphSerialization - Version Compatibility', () => {
|
||||
it('should handle version field in exports', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 1 })
|
||||
const exported = subgraph.asSerialisable()
|
||||
|
||||
// Should have version field
|
||||
expect(exported).toHaveProperty("version")
|
||||
expect(typeof exported.version).toBe("number")
|
||||
expect(exported).toHaveProperty('version')
|
||||
expect(typeof exported.version).toBe('number')
|
||||
})
|
||||
|
||||
it("should load version 1.0+ format", () => {
|
||||
it('should load version 1.0+ format', () => {
|
||||
const modernFormat = {
|
||||
version: 1, // Number as expected by current implementation
|
||||
id: "test-modern-id",
|
||||
name: "Modern Subgraph",
|
||||
id: 'test-modern-id',
|
||||
name: 'Modern Subgraph',
|
||||
nodes: [],
|
||||
links: {},
|
||||
groups: [],
|
||||
config: {},
|
||||
definitions: { subgraphs: [] },
|
||||
inputs: [{ id: "input-id", name: "modern_input", type: "number" }],
|
||||
outputs: [{ id: "output-id", name: "modern_output", type: "string" }],
|
||||
inputs: [{ id: 'input-id', name: 'modern_input', type: 'number' }],
|
||||
outputs: [{ id: 'output-id', name: 'modern_output', type: 'string' }],
|
||||
inputNode: {
|
||||
id: -10,
|
||||
bounding: [0, 0, 120, 60],
|
||||
bounding: [0, 0, 120, 60]
|
||||
},
|
||||
outputNode: {
|
||||
id: -20,
|
||||
bounding: [300, 0, 120, 60],
|
||||
bounding: [300, 0, 120, 60]
|
||||
},
|
||||
widgets: [],
|
||||
widgets: []
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
const subgraph = new Subgraph(new LGraph(), modernFormat)
|
||||
expect(subgraph.name).toBe("Modern Subgraph")
|
||||
expect(subgraph.name).toBe('Modern Subgraph')
|
||||
expect(subgraph.inputs.length).toBe(1)
|
||||
expect(subgraph.outputs.length).toBe(1)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it("should handle missing fields gracefully", () => {
|
||||
it('should handle missing fields gracefully', () => {
|
||||
const incompleteFormat = {
|
||||
version: 1,
|
||||
id: "incomplete-id",
|
||||
name: "Incomplete Subgraph",
|
||||
id: 'incomplete-id',
|
||||
name: 'Incomplete Subgraph',
|
||||
nodes: [],
|
||||
links: {},
|
||||
groups: [],
|
||||
@@ -270,29 +269,29 @@ describe("SubgraphSerialization - Version Compatibility", () => {
|
||||
definitions: { subgraphs: [] },
|
||||
inputNode: {
|
||||
id: -10,
|
||||
bounding: [0, 0, 120, 60],
|
||||
bounding: [0, 0, 120, 60]
|
||||
},
|
||||
outputNode: {
|
||||
id: -20,
|
||||
bounding: [300, 0, 120, 60],
|
||||
},
|
||||
bounding: [300, 0, 120, 60]
|
||||
}
|
||||
// Missing optional: inputs, outputs, widgets
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
const subgraph = new Subgraph(new LGraph(), incompleteFormat)
|
||||
expect(subgraph.name).toBe("Incomplete Subgraph")
|
||||
expect(subgraph.name).toBe('Incomplete Subgraph')
|
||||
// Should have default empty arrays
|
||||
expect(Array.isArray(subgraph.inputs)).toBe(true)
|
||||
expect(Array.isArray(subgraph.outputs)).toBe(true)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it("should consider future-proofing", () => {
|
||||
it('should consider future-proofing', () => {
|
||||
const futureFormat = {
|
||||
version: 2, // Future version (number)
|
||||
id: "future-id",
|
||||
name: "Future Subgraph",
|
||||
id: 'future-id',
|
||||
name: 'Future Subgraph',
|
||||
nodes: [],
|
||||
links: {},
|
||||
groups: [],
|
||||
@@ -302,34 +301,34 @@ describe("SubgraphSerialization - Version Compatibility", () => {
|
||||
outputs: [],
|
||||
inputNode: {
|
||||
id: -10,
|
||||
bounding: [0, 0, 120, 60],
|
||||
bounding: [0, 0, 120, 60]
|
||||
},
|
||||
outputNode: {
|
||||
id: -20,
|
||||
bounding: [300, 0, 120, 60],
|
||||
bounding: [300, 0, 120, 60]
|
||||
},
|
||||
widgets: [],
|
||||
futureFeature: "unknown_data", // Unknown future field
|
||||
futureFeature: 'unknown_data' // Unknown future field
|
||||
}
|
||||
|
||||
// Should handle future format gracefully
|
||||
expect(() => {
|
||||
const subgraph = new Subgraph(new LGraph(), futureFormat)
|
||||
expect(subgraph.name).toBe("Future Subgraph")
|
||||
expect(subgraph.name).toBe('Future Subgraph')
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphSerialization - Data Integrity", () => {
|
||||
it("should pass round-trip testing (save → load → save → compare)", () => {
|
||||
describe('SubgraphSerialization - Data Integrity', () => {
|
||||
it('should pass round-trip testing (save → load → save → compare)', () => {
|
||||
const original = createTestSubgraph({
|
||||
name: "Round Trip Test",
|
||||
name: 'Round Trip Test',
|
||||
nodeCount: 3,
|
||||
inputs: [
|
||||
{ name: "rt_input1", type: "number" },
|
||||
{ name: "rt_input2", type: "string" },
|
||||
{ name: 'rt_input1', type: 'number' },
|
||||
{ name: 'rt_input2', type: 'string' }
|
||||
],
|
||||
outputs: [{ name: "rt_output1", type: "boolean" }],
|
||||
outputs: [{ name: 'rt_output1', type: 'boolean' }]
|
||||
})
|
||||
|
||||
// First round trip
|
||||
@@ -359,9 +358,9 @@ describe("SubgraphSerialization - Data Integrity", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("should verify IDs remain unique", () => {
|
||||
const subgraph1 = createTestSubgraph({ name: "Unique1", nodeCount: 2 })
|
||||
const subgraph2 = createTestSubgraph({ name: "Unique2", nodeCount: 2 })
|
||||
it('should verify IDs remain unique', () => {
|
||||
const subgraph1 = createTestSubgraph({ name: 'Unique1', nodeCount: 2 })
|
||||
const subgraph2 = createTestSubgraph({ name: 'Unique2', nodeCount: 2 })
|
||||
|
||||
const exported1 = subgraph1.asSerialisable()
|
||||
const exported2 = subgraph2.asSerialisable()
|
||||
@@ -377,10 +376,10 @@ describe("SubgraphSerialization - Data Integrity", () => {
|
||||
expect(restored2.id).toBe(subgraph2.id)
|
||||
})
|
||||
|
||||
it("should maintain connection integrity after load", () => {
|
||||
it('should maintain connection integrity after load', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 2 })
|
||||
subgraph.addInput("connection_test", "number")
|
||||
subgraph.addOutput("connection_result", "string")
|
||||
subgraph.addInput('connection_test', 'number')
|
||||
subgraph.addOutput('connection_result', 'string')
|
||||
|
||||
const exported = subgraph.asSerialisable()
|
||||
const restored = new Subgraph(new LGraph(), exported)
|
||||
@@ -388,8 +387,8 @@ describe("SubgraphSerialization - Data Integrity", () => {
|
||||
// Verify I/O connections can be established
|
||||
expect(restored.inputs.length).toBe(1)
|
||||
expect(restored.outputs.length).toBe(1)
|
||||
expect(restored.inputs[0].name).toBe("connection_test")
|
||||
expect(restored.outputs[0].name).toBe("connection_result")
|
||||
expect(restored.inputs[0].name).toBe('connection_test')
|
||||
expect(restored.outputs[0].name).toBe('connection_result')
|
||||
|
||||
// Verify subgraph can be instantiated
|
||||
const instance = createTestSubgraphNode(restored)
|
||||
@@ -397,16 +396,16 @@ describe("SubgraphSerialization - Data Integrity", () => {
|
||||
expect(instance.outputs.length).toBe(1)
|
||||
})
|
||||
|
||||
it("should preserve node positions and properties", () => {
|
||||
it('should preserve node positions and properties', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 2 })
|
||||
|
||||
// Modify node positions if possible
|
||||
if (subgraph.nodes.length > 0) {
|
||||
const node = subgraph.nodes[0]
|
||||
if ("pos" in node) {
|
||||
if ('pos' in node) {
|
||||
node.pos = [100, 200]
|
||||
}
|
||||
if ("size" in node) {
|
||||
if ('size' in node) {
|
||||
node.size = [150, 80]
|
||||
}
|
||||
}
|
||||
@@ -424,7 +423,7 @@ describe("SubgraphSerialization - Data Integrity", () => {
|
||||
expect(restoredNode).toBeDefined()
|
||||
|
||||
// Properties should be preserved if supported
|
||||
if ("pos" in restoredNode && restoredNode.pos) {
|
||||
if ('pos' in restoredNode && restoredNode.pos) {
|
||||
expect(Array.isArray(restoredNode.pos)).toBe(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LinkConnector } from "@/lib/litegraph/src/canvas/LinkConnector"
|
||||
import { ToInputFromIoNodeLink } from "@/lib/litegraph/src/canvas/ToInputFromIoNodeLink"
|
||||
import { SUBGRAPH_INPUT_ID } from "@/lib/litegraph/src/constants"
|
||||
import { LGraphNode, type LinkNetwork } from "@/lib/litegraph/src/litegraph"
|
||||
import { NodeInputSlot } from "@/lib/litegraph/src/node/NodeInputSlot"
|
||||
import { NodeOutputSlot } from "@/lib/litegraph/src/node/NodeOutputSlot"
|
||||
import { isSubgraphInput, isSubgraphOutput } from "@/lib/litegraph/src/subgraph/subgraphUtils"
|
||||
import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||
import { ToInputFromIoNodeLink } from '@/lib/litegraph/src/canvas/ToInputFromIoNodeLink'
|
||||
import { SUBGRAPH_INPUT_ID } from '@/lib/litegraph/src/constants'
|
||||
import { LGraphNode, type LinkNetwork } from '@/lib/litegraph/src/litegraph'
|
||||
import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot'
|
||||
import { NodeOutputSlot } from '@/lib/litegraph/src/node/NodeOutputSlot'
|
||||
import {
|
||||
isSubgraphInput,
|
||||
isSubgraphOutput
|
||||
} from '@/lib/litegraph/src/subgraph/subgraphUtils'
|
||||
|
||||
import { createTestSubgraph, createTestSubgraphNode } from "./fixtures/subgraphHelpers"
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("Subgraph slot connections", () => {
|
||||
describe("SubgraphInput connections", () => {
|
||||
it("should connect to compatible regular input slots", () => {
|
||||
describe('Subgraph slot connections', () => {
|
||||
describe('SubgraphInput connections', () => {
|
||||
it('should connect to compatible regular input slots', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "test_input", type: "number" }],
|
||||
inputs: [{ name: 'test_input', type: 'number' }]
|
||||
})
|
||||
|
||||
const subgraphInput = subgraph.inputs[0]
|
||||
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addInput("compatible_input", "number")
|
||||
node.addInput("incompatible_input", "string")
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addInput('compatible_input', 'number')
|
||||
node.addInput('incompatible_input', 'string')
|
||||
subgraph.add(node)
|
||||
|
||||
const compatibleSlot = node.inputs[0] as NodeInputSlot
|
||||
@@ -44,12 +50,12 @@ describe("Subgraph slot connections", () => {
|
||||
// expect(subgraphOutput.isValidTarget(subgraphInput)).toBe(true)
|
||||
// })
|
||||
|
||||
it("should not connect to another SubgraphInput", () => {
|
||||
it('should not connect to another SubgraphInput', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: "input1", type: "number" },
|
||||
{ name: "input2", type: "number" },
|
||||
],
|
||||
{ name: 'input1', type: 'number' },
|
||||
{ name: 'input2', type: 'number' }
|
||||
]
|
||||
})
|
||||
|
||||
const subgraphInput1 = subgraph.inputs[0]
|
||||
@@ -58,15 +64,15 @@ describe("Subgraph slot connections", () => {
|
||||
expect(subgraphInput2.isValidTarget(subgraphInput1)).toBe(false)
|
||||
})
|
||||
|
||||
it("should not connect to output slots", () => {
|
||||
it('should not connect to output slots', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "test_input", type: "number" }],
|
||||
inputs: [{ name: 'test_input', type: 'number' }]
|
||||
})
|
||||
|
||||
const subgraphInput = subgraph.inputs[0]
|
||||
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addOutput("test_output", "number")
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addOutput('test_output', 'number')
|
||||
subgraph.add(node)
|
||||
const outputSlot = node.outputs[0] as NodeOutputSlot
|
||||
|
||||
@@ -74,53 +80,56 @@ describe("Subgraph slot connections", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphOutput connections", () => {
|
||||
it("should connect from compatible regular output slots", () => {
|
||||
describe('SubgraphOutput connections', () => {
|
||||
it('should connect from compatible regular output slots', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addOutput("out", "number")
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addOutput('out', 'number')
|
||||
subgraph.add(node)
|
||||
|
||||
const subgraphOutput = subgraph.addOutput("result", "number")
|
||||
const subgraphOutput = subgraph.addOutput('result', 'number')
|
||||
const nodeOutput = node.outputs[0]
|
||||
|
||||
expect(subgraphOutput.isValidTarget(nodeOutput)).toBe(true)
|
||||
})
|
||||
|
||||
it("should connect from SubgraphInput", () => {
|
||||
it('should connect from SubgraphInput', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
const subgraphInput = subgraph.addInput("value", "number")
|
||||
const subgraphOutput = subgraph.addOutput("result", "number")
|
||||
const subgraphInput = subgraph.addInput('value', 'number')
|
||||
const subgraphOutput = subgraph.addOutput('result', 'number')
|
||||
|
||||
expect(subgraphOutput.isValidTarget(subgraphInput)).toBe(true)
|
||||
})
|
||||
|
||||
it("should not connect to another SubgraphOutput", () => {
|
||||
it('should not connect to another SubgraphOutput', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
const subgraphOutput1 = subgraph.addOutput("result1", "number")
|
||||
const subgraphOutput2 = subgraph.addOutput("result2", "number")
|
||||
const subgraphOutput1 = subgraph.addOutput('result1', 'number')
|
||||
const subgraphOutput2 = subgraph.addOutput('result2', 'number')
|
||||
|
||||
expect(subgraphOutput1.isValidTarget(subgraphOutput2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("LinkConnector dragging behavior", () => {
|
||||
it("should drag existing link when dragging from input slot connected to subgraph input node", () => {
|
||||
describe('LinkConnector dragging behavior', () => {
|
||||
it('should drag existing link when dragging from input slot connected to subgraph input node', () => {
|
||||
// Create a subgraph with one input
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input1", type: "number" }],
|
||||
inputs: [{ name: 'input1', type: 'number' }]
|
||||
})
|
||||
|
||||
// Create a node inside the subgraph
|
||||
const internalNode = new LGraphNode("InternalNode")
|
||||
const internalNode = new LGraphNode('InternalNode')
|
||||
internalNode.id = 100
|
||||
internalNode.addInput("in", "number")
|
||||
internalNode.addInput('in', 'number')
|
||||
subgraph.add(internalNode)
|
||||
|
||||
// Connect the subgraph input to the internal node's input
|
||||
const link = subgraph.inputNode.slots[0].connect(internalNode.inputs[0], internalNode)
|
||||
const link = subgraph.inputNode.slots[0].connect(
|
||||
internalNode.inputs[0],
|
||||
internalNode
|
||||
)
|
||||
expect(link).toBeDefined()
|
||||
expect(link!.origin_id).toBe(SUBGRAPH_INPUT_ID)
|
||||
expect(link!.target_id).toBe(internalNode.id)
|
||||
@@ -137,7 +146,7 @@ describe("Subgraph slot connections", () => {
|
||||
|
||||
// Verify that we're dragging the existing link
|
||||
expect(connector.isConnecting).toBe(true)
|
||||
expect(connector.state.connectingTo).toBe("input")
|
||||
expect(connector.state.connectingTo).toBe('input')
|
||||
expect(connector.state.draggingExistingLinks).toBe(true)
|
||||
|
||||
// Check that we have exactly one render link
|
||||
@@ -155,19 +164,19 @@ describe("Subgraph slot connections", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Type compatibility", () => {
|
||||
it("should respect type compatibility for SubgraphInput connections", () => {
|
||||
describe('Type compatibility', () => {
|
||||
it('should respect type compatibility for SubgraphInput connections', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "number_input", type: "number" }],
|
||||
inputs: [{ name: 'number_input', type: 'number' }]
|
||||
})
|
||||
|
||||
const subgraphInput = subgraph.inputs[0]
|
||||
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addInput("number_slot", "number")
|
||||
node.addInput("string_slot", "string")
|
||||
node.addInput("any_slot", "*")
|
||||
node.addInput("boolean_slot", "boolean")
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addInput('number_slot', 'number')
|
||||
node.addInput('string_slot', 'string')
|
||||
node.addInput('any_slot', '*')
|
||||
node.addInput('boolean_slot', 'boolean')
|
||||
subgraph.add(node)
|
||||
|
||||
const numberSlot = node.inputs[0] as NodeInputSlot
|
||||
@@ -181,27 +190,27 @@ describe("Subgraph slot connections", () => {
|
||||
expect(booleanSlot.isValidTarget(subgraphInput)).toBe(false)
|
||||
})
|
||||
|
||||
it("should respect type compatibility for SubgraphOutput connections", () => {
|
||||
it('should respect type compatibility for SubgraphOutput connections', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addOutput("out", "string")
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addOutput('out', 'string')
|
||||
subgraph.add(node)
|
||||
|
||||
const subgraphOutput = subgraph.addOutput("result", "number")
|
||||
const subgraphOutput = subgraph.addOutput('result', 'number')
|
||||
const nodeOutput = node.outputs[0]
|
||||
|
||||
expect(subgraphOutput.isValidTarget(nodeOutput)).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle wildcard SubgraphInput", () => {
|
||||
it('should handle wildcard SubgraphInput', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "any_input", type: "*" }],
|
||||
inputs: [{ name: 'any_input', type: '*' }]
|
||||
})
|
||||
|
||||
const subgraphInput = subgraph.inputs[0]
|
||||
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addInput("number_slot", "number")
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addInput('number_slot', 'number')
|
||||
subgraph.add(node)
|
||||
|
||||
const numberSlot = node.inputs[0] as NodeInputSlot
|
||||
@@ -210,12 +219,12 @@ describe("Subgraph slot connections", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Type guards", () => {
|
||||
it("should correctly identify SubgraphInput", () => {
|
||||
describe('Type guards', () => {
|
||||
it('should correctly identify SubgraphInput', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphInput = subgraph.addInput("value", "number")
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addInput("in", "number")
|
||||
const subgraphInput = subgraph.addInput('value', 'number')
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addInput('in', 'number')
|
||||
|
||||
expect(isSubgraphInput(subgraphInput)).toBe(true)
|
||||
expect(isSubgraphInput(node.inputs[0])).toBe(false)
|
||||
@@ -225,11 +234,11 @@ describe("Subgraph slot connections", () => {
|
||||
expect(isSubgraphInput({})).toBe(false)
|
||||
})
|
||||
|
||||
it("should correctly identify SubgraphOutput", () => {
|
||||
it('should correctly identify SubgraphOutput', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphOutput = subgraph.addOutput("result", "number")
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addOutput("out", "number")
|
||||
const subgraphOutput = subgraph.addOutput('result', 'number')
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addOutput('out', 'number')
|
||||
|
||||
expect(isSubgraphOutput(subgraphOutput)).toBe(true)
|
||||
expect(isSubgraphOutput(node.outputs[0])).toBe(false)
|
||||
@@ -240,23 +249,23 @@ describe("Subgraph slot connections", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Nested subgraphs", () => {
|
||||
it("should handle dragging from SubgraphInput in nested subgraphs", () => {
|
||||
describe('Nested subgraphs', () => {
|
||||
it('should handle dragging from SubgraphInput in nested subgraphs', () => {
|
||||
const parentSubgraph = createTestSubgraph({
|
||||
inputs: [{ name: "parent_input", type: "number" }],
|
||||
outputs: [{ name: "parent_output", type: "number" }],
|
||||
inputs: [{ name: 'parent_input', type: 'number' }],
|
||||
outputs: [{ name: 'parent_output', type: 'number' }]
|
||||
})
|
||||
|
||||
const nestedSubgraph = createTestSubgraph({
|
||||
inputs: [{ name: "nested_input", type: "number" }],
|
||||
outputs: [{ name: "nested_output", type: "number" }],
|
||||
inputs: [{ name: 'nested_input', type: 'number' }],
|
||||
outputs: [{ name: 'nested_output', type: 'number' }]
|
||||
})
|
||||
|
||||
const nestedSubgraphNode = createTestSubgraphNode(nestedSubgraph)
|
||||
parentSubgraph.add(nestedSubgraphNode)
|
||||
|
||||
const regularNode = new LGraphNode("TestNode")
|
||||
regularNode.addInput("test_input", "number")
|
||||
const regularNode = new LGraphNode('TestNode')
|
||||
regularNode.addInput('test_input', 'number')
|
||||
nestedSubgraph.add(regularNode)
|
||||
|
||||
const nestedSubgraphInput = nestedSubgraph.inputs[0]
|
||||
@@ -265,18 +274,18 @@ describe("Subgraph slot connections", () => {
|
||||
expect(regularNodeSlot.isValidTarget(nestedSubgraphInput)).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle multiple levels of nesting", () => {
|
||||
it('should handle multiple levels of nesting', () => {
|
||||
const level1 = createTestSubgraph({
|
||||
inputs: [{ name: "level1_input", type: "string" }],
|
||||
inputs: [{ name: 'level1_input', type: 'string' }]
|
||||
})
|
||||
|
||||
const level2 = createTestSubgraph({
|
||||
inputs: [{ name: "level2_input", type: "string" }],
|
||||
inputs: [{ name: 'level2_input', type: 'string' }]
|
||||
})
|
||||
|
||||
const level3 = createTestSubgraph({
|
||||
inputs: [{ name: "level3_input", type: "string" }],
|
||||
outputs: [{ name: "level3_output", type: "string" }],
|
||||
inputs: [{ name: 'level3_input', type: 'string' }],
|
||||
outputs: [{ name: 'level3_output', type: 'string' }]
|
||||
})
|
||||
|
||||
const level2Node = createTestSubgraphNode(level2)
|
||||
@@ -285,8 +294,8 @@ describe("Subgraph slot connections", () => {
|
||||
const level3Node = createTestSubgraphNode(level3)
|
||||
level2.add(level3Node)
|
||||
|
||||
const deepNode = new LGraphNode("DeepNode")
|
||||
deepNode.addInput("deep_input", "string")
|
||||
const deepNode = new LGraphNode('DeepNode')
|
||||
deepNode.addInput('deep_input', 'string')
|
||||
level3.add(deepNode)
|
||||
|
||||
const level3Input = level3.inputs[0]
|
||||
@@ -298,24 +307,24 @@ describe("Subgraph slot connections", () => {
|
||||
expect(level3Output.isValidTarget(level3Input)).toBe(true)
|
||||
})
|
||||
|
||||
it("should maintain type checking across nesting levels", () => {
|
||||
it('should maintain type checking across nesting levels', () => {
|
||||
const outer = createTestSubgraph({
|
||||
inputs: [{ name: "outer_number", type: "number" }],
|
||||
inputs: [{ name: 'outer_number', type: 'number' }]
|
||||
})
|
||||
|
||||
const inner = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: "inner_number", type: "number" },
|
||||
{ name: "inner_string", type: "string" },
|
||||
],
|
||||
{ name: 'inner_number', type: 'number' },
|
||||
{ name: 'inner_string', type: 'string' }
|
||||
]
|
||||
})
|
||||
|
||||
const innerNode = createTestSubgraphNode(inner)
|
||||
outer.add(innerNode)
|
||||
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addInput("number_slot", "number")
|
||||
node.addInput("string_slot", "string")
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addInput('number_slot', 'number')
|
||||
node.addInput('string_slot', 'string')
|
||||
inner.add(node)
|
||||
|
||||
const innerNumberInput = inner.inputs[0]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphNode } from "@/lib/litegraph/src/litegraph"
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { createTestSubgraph } from "./fixtures/subgraphHelpers"
|
||||
import { createTestSubgraph } from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("SubgraphSlot visual feedback", () => {
|
||||
describe('SubgraphSlot visual feedback', () => {
|
||||
let mockCtx: CanvasRenderingContext2D
|
||||
let mockColorContext: any
|
||||
let globalAlphaValues: number[]
|
||||
@@ -23,35 +23,35 @@ describe("SubgraphSlot visual feedback", () => {
|
||||
this._globalAlpha = value
|
||||
globalAlphaValues.push(value)
|
||||
},
|
||||
fillStyle: "",
|
||||
strokeStyle: "",
|
||||
fillStyle: '',
|
||||
strokeStyle: '',
|
||||
lineWidth: 1,
|
||||
beginPath: vi.fn(),
|
||||
arc: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
rect: vi.fn(),
|
||||
fillText: vi.fn(),
|
||||
fillText: vi.fn()
|
||||
}
|
||||
mockCtx = mockContext as unknown as CanvasRenderingContext2D
|
||||
|
||||
// Create a mock color context
|
||||
mockColorContext = {
|
||||
defaultInputColor: "#FF0000",
|
||||
defaultOutputColor: "#00FF00",
|
||||
getConnectedColor: vi.fn().mockReturnValue("#0000FF"),
|
||||
getDisconnectedColor: vi.fn().mockReturnValue("#AAAAAA"),
|
||||
defaultInputColor: '#FF0000',
|
||||
defaultOutputColor: '#00FF00',
|
||||
getConnectedColor: vi.fn().mockReturnValue('#0000FF'),
|
||||
getDisconnectedColor: vi.fn().mockReturnValue('#AAAAAA')
|
||||
}
|
||||
})
|
||||
|
||||
it("should render SubgraphInput slots with full opacity when dragging from compatible slot", () => {
|
||||
it('should render SubgraphInput slots with full opacity when dragging from compatible slot', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addInput("in", "number")
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addInput('in', 'number')
|
||||
subgraph.add(node)
|
||||
|
||||
// Add a subgraph input
|
||||
const subgraphInput = subgraph.addInput("value", "number")
|
||||
const subgraphInput = subgraph.addInput('value', 'number')
|
||||
|
||||
// Simulate dragging from the subgraph input (which acts as output inside subgraph)
|
||||
const nodeInput = node.inputs[0]
|
||||
@@ -61,7 +61,7 @@ describe("SubgraphSlot visual feedback", () => {
|
||||
ctx: mockCtx,
|
||||
colorContext: mockColorContext,
|
||||
fromSlot: nodeInput,
|
||||
editorAlpha: 1,
|
||||
editorAlpha: 1
|
||||
})
|
||||
|
||||
// Should render with full opacity (not 0.4)
|
||||
@@ -69,19 +69,19 @@ describe("SubgraphSlot visual feedback", () => {
|
||||
expect(globalAlphaValues).not.toContain(0.4)
|
||||
})
|
||||
|
||||
it("should render SubgraphInput slots with 40% opacity when dragging from another SubgraphInput", () => {
|
||||
it('should render SubgraphInput slots with 40% opacity when dragging from another SubgraphInput', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// Add two subgraph inputs
|
||||
const subgraphInput1 = subgraph.addInput("value1", "number")
|
||||
const subgraphInput2 = subgraph.addInput("value2", "number")
|
||||
const subgraphInput1 = subgraph.addInput('value1', 'number')
|
||||
const subgraphInput2 = subgraph.addInput('value2', 'number')
|
||||
|
||||
// Draw subgraphInput2 while dragging from subgraphInput1 (incompatible - both are outputs inside subgraph)
|
||||
subgraphInput2.draw({
|
||||
ctx: mockCtx,
|
||||
colorContext: mockColorContext,
|
||||
fromSlot: subgraphInput1,
|
||||
editorAlpha: 1,
|
||||
editorAlpha: 1
|
||||
})
|
||||
|
||||
// Should render with 40% opacity
|
||||
@@ -89,14 +89,14 @@ describe("SubgraphSlot visual feedback", () => {
|
||||
expect(globalAlphaValues).toContain(0.4)
|
||||
})
|
||||
|
||||
it("should render SubgraphOutput slots with full opacity when dragging from compatible slot", () => {
|
||||
it('should render SubgraphOutput slots with full opacity when dragging from compatible slot', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addOutput("out", "number")
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addOutput('out', 'number')
|
||||
subgraph.add(node)
|
||||
|
||||
// Add a subgraph output
|
||||
const subgraphOutput = subgraph.addOutput("result", "number")
|
||||
const subgraphOutput = subgraph.addOutput('result', 'number')
|
||||
|
||||
// Simulate dragging from a node output
|
||||
const nodeOutput = node.outputs[0]
|
||||
@@ -106,7 +106,7 @@ describe("SubgraphSlot visual feedback", () => {
|
||||
ctx: mockCtx,
|
||||
colorContext: mockColorContext,
|
||||
fromSlot: nodeOutput,
|
||||
editorAlpha: 1,
|
||||
editorAlpha: 1
|
||||
})
|
||||
|
||||
// Should render with full opacity (not 0.4)
|
||||
@@ -114,19 +114,19 @@ describe("SubgraphSlot visual feedback", () => {
|
||||
expect(globalAlphaValues).not.toContain(0.4)
|
||||
})
|
||||
|
||||
it("should render SubgraphOutput slots with 40% opacity when dragging from another SubgraphOutput", () => {
|
||||
it('should render SubgraphOutput slots with 40% opacity when dragging from another SubgraphOutput', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// Add two subgraph outputs
|
||||
const subgraphOutput1 = subgraph.addOutput("result1", "number")
|
||||
const subgraphOutput2 = subgraph.addOutput("result2", "number")
|
||||
const subgraphOutput1 = subgraph.addOutput('result1', 'number')
|
||||
const subgraphOutput2 = subgraph.addOutput('result2', 'number')
|
||||
|
||||
// Draw subgraphOutput2 while dragging from subgraphOutput1 (incompatible - both are inputs inside subgraph)
|
||||
subgraphOutput2.draw({
|
||||
ctx: mockCtx,
|
||||
colorContext: mockColorContext,
|
||||
fromSlot: subgraphOutput1,
|
||||
editorAlpha: 1,
|
||||
editorAlpha: 1
|
||||
})
|
||||
|
||||
// Should render with 40% opacity
|
||||
@@ -154,14 +154,14 @@ describe("SubgraphSlot visual feedback", () => {
|
||||
// expect(mockCtx.globalAlpha).toBe(1)
|
||||
// })
|
||||
|
||||
it("should render slots with 40% opacity when dragging between incompatible types", () => {
|
||||
it('should render slots with 40% opacity when dragging between incompatible types', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addOutput("string_output", "string")
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.addOutput('string_output', 'string')
|
||||
subgraph.add(node)
|
||||
|
||||
// Add subgraph output with incompatible type
|
||||
const subgraphOutput = subgraph.addOutput("result", "number")
|
||||
const subgraphOutput = subgraph.addOutput('result', 'number')
|
||||
|
||||
// Get the string output slot from the node
|
||||
const nodeStringOutput = node.outputs[0]
|
||||
@@ -171,7 +171,7 @@ describe("SubgraphSlot visual feedback", () => {
|
||||
ctx: mockCtx,
|
||||
colorContext: mockColorContext,
|
||||
fromSlot: nodeStringOutput,
|
||||
editorAlpha: 1,
|
||||
editorAlpha: 1
|
||||
})
|
||||
|
||||
// Should render with 40% opacity due to type mismatch
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import type { ISlotType } from "@/lib/litegraph/src/interfaces"
|
||||
import type { TWidgetType } from "@/lib/litegraph/src/types/widgets"
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
import type { ISlotType } from '@/lib/litegraph/src/interfaces'
|
||||
import { LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { TWidgetType } from '@/lib/litegraph/src/types/widgets'
|
||||
import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget'
|
||||
|
||||
import { LGraphNode, Subgraph } from "@/lib/litegraph/src/litegraph"
|
||||
import { BaseWidget } from "@/lib/litegraph/src/widgets/BaseWidget"
|
||||
|
||||
import { createEventCapture, createTestSubgraph, createTestSubgraphNode } from "./fixtures/subgraphHelpers"
|
||||
import {
|
||||
createEventCapture,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
// Helper to create a node with a widget
|
||||
function createNodeWithWidget(
|
||||
title: string,
|
||||
widgetType: TWidgetType = "number",
|
||||
widgetType: TWidgetType = 'number',
|
||||
widgetValue: any = 42,
|
||||
slotType: ISlotType = "number",
|
||||
tooltip?: string,
|
||||
slotType: ISlotType = 'number',
|
||||
tooltip?: string
|
||||
) {
|
||||
const node = new LGraphNode(title)
|
||||
const input = node.addInput("value", slotType)
|
||||
node.addOutput("out", slotType)
|
||||
const input = node.addInput('value', slotType)
|
||||
node.addOutput('out', slotType)
|
||||
|
||||
const widget = new BaseWidget({
|
||||
name: "widget",
|
||||
name: 'widget',
|
||||
type: widgetType,
|
||||
value: widgetValue,
|
||||
y: 0,
|
||||
options: widgetType === "number" ? { min: 0, max: 100, step: 1 } : {},
|
||||
options: widgetType === 'number' ? { min: 0, max: 100, step: 1 } : {},
|
||||
node,
|
||||
tooltip,
|
||||
tooltip
|
||||
})
|
||||
node.widgets = [widget]
|
||||
input.widget = { name: widget.name }
|
||||
@@ -36,42 +39,60 @@ function createNodeWithWidget(
|
||||
}
|
||||
|
||||
// Helper to connect subgraph input to node and create SubgraphNode
|
||||
function setupPromotedWidget(subgraph: Subgraph, node: LGraphNode, slotIndex = 0) {
|
||||
function setupPromotedWidget(
|
||||
subgraph: Subgraph,
|
||||
node: LGraphNode,
|
||||
slotIndex = 0
|
||||
) {
|
||||
subgraph.add(node)
|
||||
subgraph.inputNode.slots[slotIndex].connect(node.inputs[slotIndex], node)
|
||||
return createTestSubgraphNode(subgraph)
|
||||
}
|
||||
|
||||
describe("SubgraphWidgetPromotion", () => {
|
||||
describe("Widget Promotion Functionality", () => {
|
||||
it("should promote widgets when connecting node to subgraph input", () => {
|
||||
describe('SubgraphWidgetPromotion', () => {
|
||||
describe('Widget Promotion Functionality', () => {
|
||||
it('should promote widgets when connecting node to subgraph input', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "value", type: "number" }],
|
||||
inputs: [{ name: 'value', type: 'number' }]
|
||||
})
|
||||
|
||||
const { node } = createNodeWithWidget("Test Node")
|
||||
const { node } = createNodeWithWidget('Test Node')
|
||||
const subgraphNode = setupPromotedWidget(subgraph, node)
|
||||
|
||||
// The widget should be promoted to the subgraph node
|
||||
expect(subgraphNode.widgets).toHaveLength(1)
|
||||
expect(subgraphNode.widgets[0].name).toBe("value") // Uses subgraph input name
|
||||
expect(subgraphNode.widgets[0].type).toBe("number")
|
||||
expect(subgraphNode.widgets[0].name).toBe('value') // Uses subgraph input name
|
||||
expect(subgraphNode.widgets[0].type).toBe('number')
|
||||
expect(subgraphNode.widgets[0].value).toBe(42)
|
||||
})
|
||||
|
||||
it("should promote all widget types", () => {
|
||||
it('should promote all widget types', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: "numberInput", type: "number" },
|
||||
{ name: "stringInput", type: "string" },
|
||||
{ name: "toggleInput", type: "boolean" },
|
||||
],
|
||||
{ name: 'numberInput', type: 'number' },
|
||||
{ name: 'stringInput', type: 'string' },
|
||||
{ name: 'toggleInput', type: 'boolean' }
|
||||
]
|
||||
})
|
||||
|
||||
// Create nodes with different widget types
|
||||
const { node: numberNode } = createNodeWithWidget("Number Node", "number", 100)
|
||||
const { node: stringNode } = createNodeWithWidget("String Node", "string", "test", "string")
|
||||
const { node: toggleNode } = createNodeWithWidget("Toggle Node", "toggle", true, "boolean")
|
||||
const { node: numberNode } = createNodeWithWidget(
|
||||
'Number Node',
|
||||
'number',
|
||||
100
|
||||
)
|
||||
const { node: stringNode } = createNodeWithWidget(
|
||||
'String Node',
|
||||
'string',
|
||||
'test',
|
||||
'string'
|
||||
)
|
||||
const { node: toggleNode } = createNodeWithWidget(
|
||||
'Toggle Node',
|
||||
'toggle',
|
||||
true,
|
||||
'boolean'
|
||||
)
|
||||
|
||||
// Setup all nodes
|
||||
subgraph.add(numberNode)
|
||||
@@ -89,25 +110,25 @@ describe("SubgraphWidgetPromotion", () => {
|
||||
|
||||
// Check specific widget values
|
||||
expect(subgraphNode.widgets[0].value).toBe(100)
|
||||
expect(subgraphNode.widgets[1].value).toBe("test")
|
||||
expect(subgraphNode.widgets[1].value).toBe('test')
|
||||
expect(subgraphNode.widgets[2].value).toBe(true)
|
||||
})
|
||||
|
||||
it("should fire widget-promoted event when widget is promoted", () => {
|
||||
it('should fire widget-promoted event when widget is promoted', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input", type: "number" }],
|
||||
inputs: [{ name: 'input', type: 'number' }]
|
||||
})
|
||||
|
||||
const eventCapture = createEventCapture(subgraph.events, [
|
||||
"widget-promoted",
|
||||
"widget-demoted",
|
||||
'widget-promoted',
|
||||
'widget-demoted'
|
||||
])
|
||||
|
||||
const { node } = createNodeWithWidget("Test Node")
|
||||
const { node } = createNodeWithWidget('Test Node')
|
||||
const subgraphNode = setupPromotedWidget(subgraph, node)
|
||||
|
||||
// Check event was fired
|
||||
const promotedEvents = eventCapture.getEventsByType("widget-promoted")
|
||||
const promotedEvents = eventCapture.getEventsByType('widget-promoted')
|
||||
expect(promotedEvents).toHaveLength(1)
|
||||
expect(promotedEvents[0].detail.widget).toBeDefined()
|
||||
expect(promotedEvents[0].detail.subgraphNode).toBe(subgraphNode)
|
||||
@@ -115,22 +136,24 @@ describe("SubgraphWidgetPromotion", () => {
|
||||
eventCapture.cleanup()
|
||||
})
|
||||
|
||||
it("should fire widget-demoted event when removing promoted widget", () => {
|
||||
it('should fire widget-demoted event when removing promoted widget', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input", type: "number" }],
|
||||
inputs: [{ name: 'input', type: 'number' }]
|
||||
})
|
||||
|
||||
const { node } = createNodeWithWidget("Test Node")
|
||||
const { node } = createNodeWithWidget('Test Node')
|
||||
const subgraphNode = setupPromotedWidget(subgraph, node)
|
||||
expect(subgraphNode.widgets).toHaveLength(1)
|
||||
|
||||
const eventCapture = createEventCapture(subgraph.events, ["widget-demoted"])
|
||||
const eventCapture = createEventCapture(subgraph.events, [
|
||||
'widget-demoted'
|
||||
])
|
||||
|
||||
// Remove the widget
|
||||
subgraphNode.removeWidgetByName("input")
|
||||
subgraphNode.removeWidgetByName('input')
|
||||
|
||||
// Check event was fired
|
||||
const demotedEvents = eventCapture.getEventsByType("widget-demoted")
|
||||
const demotedEvents = eventCapture.getEventsByType('widget-demoted')
|
||||
expect(demotedEvents).toHaveLength(1)
|
||||
expect(demotedEvents[0].detail.widget).toBeDefined()
|
||||
expect(demotedEvents[0].detail.subgraphNode).toBe(subgraphNode)
|
||||
@@ -141,35 +164,35 @@ describe("SubgraphWidgetPromotion", () => {
|
||||
eventCapture.cleanup()
|
||||
})
|
||||
|
||||
it("should handle multiple widgets on same node", () => {
|
||||
it('should handle multiple widgets on same node', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: "input1", type: "number" },
|
||||
{ name: "input2", type: "string" },
|
||||
],
|
||||
{ name: 'input1', type: 'number' },
|
||||
{ name: 'input2', type: 'string' }
|
||||
]
|
||||
})
|
||||
|
||||
// Create node with multiple widgets
|
||||
const multiWidgetNode = new LGraphNode("Multi Widget Node")
|
||||
const numInput = multiWidgetNode.addInput("num", "number")
|
||||
const strInput = multiWidgetNode.addInput("str", "string")
|
||||
const multiWidgetNode = new LGraphNode('Multi Widget Node')
|
||||
const numInput = multiWidgetNode.addInput('num', 'number')
|
||||
const strInput = multiWidgetNode.addInput('str', 'string')
|
||||
|
||||
const widget1 = new BaseWidget({
|
||||
name: "widget1",
|
||||
type: "number",
|
||||
name: 'widget1',
|
||||
type: 'number',
|
||||
value: 10,
|
||||
y: 0,
|
||||
options: {},
|
||||
node: multiWidgetNode,
|
||||
node: multiWidgetNode
|
||||
})
|
||||
|
||||
const widget2 = new BaseWidget({
|
||||
name: "widget2",
|
||||
type: "string",
|
||||
value: "hello",
|
||||
name: 'widget2',
|
||||
type: 'string',
|
||||
value: 'hello',
|
||||
y: 40,
|
||||
options: {},
|
||||
node: multiWidgetNode,
|
||||
node: multiWidgetNode
|
||||
})
|
||||
|
||||
multiWidgetNode.widgets = [widget1, widget2]
|
||||
@@ -178,49 +201,57 @@ describe("SubgraphWidgetPromotion", () => {
|
||||
subgraph.add(multiWidgetNode)
|
||||
|
||||
// Connect both inputs
|
||||
subgraph.inputNode.slots[0].connect(multiWidgetNode.inputs[0], multiWidgetNode)
|
||||
subgraph.inputNode.slots[1].connect(multiWidgetNode.inputs[1], multiWidgetNode)
|
||||
subgraph.inputNode.slots[0].connect(
|
||||
multiWidgetNode.inputs[0],
|
||||
multiWidgetNode
|
||||
)
|
||||
subgraph.inputNode.slots[1].connect(
|
||||
multiWidgetNode.inputs[1],
|
||||
multiWidgetNode
|
||||
)
|
||||
|
||||
// Create SubgraphNode
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Both widgets should be promoted
|
||||
expect(subgraphNode.widgets).toHaveLength(2)
|
||||
expect(subgraphNode.widgets[0].name).toBe("input1")
|
||||
expect(subgraphNode.widgets[0].name).toBe('input1')
|
||||
expect(subgraphNode.widgets[0].value).toBe(10)
|
||||
|
||||
expect(subgraphNode.widgets[1].name).toBe("input2")
|
||||
expect(subgraphNode.widgets[1].value).toBe("hello")
|
||||
expect(subgraphNode.widgets[1].name).toBe('input2')
|
||||
expect(subgraphNode.widgets[1].value).toBe('hello')
|
||||
})
|
||||
|
||||
it("should fire widget-demoted events when node is removed", () => {
|
||||
it('should fire widget-demoted events when node is removed', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input", type: "number" }],
|
||||
inputs: [{ name: 'input', type: 'number' }]
|
||||
})
|
||||
|
||||
const { node } = createNodeWithWidget("Test Node")
|
||||
const { node } = createNodeWithWidget('Test Node')
|
||||
const subgraphNode = setupPromotedWidget(subgraph, node)
|
||||
|
||||
expect(subgraphNode.widgets).toHaveLength(1)
|
||||
|
||||
const eventCapture = createEventCapture(subgraph.events, ["widget-demoted"])
|
||||
const eventCapture = createEventCapture(subgraph.events, [
|
||||
'widget-demoted'
|
||||
])
|
||||
|
||||
// Remove the subgraph node
|
||||
subgraphNode.onRemoved()
|
||||
|
||||
// Should fire demoted events for all widgets
|
||||
const demotedEvents = eventCapture.getEventsByType("widget-demoted")
|
||||
const demotedEvents = eventCapture.getEventsByType('widget-demoted')
|
||||
expect(demotedEvents).toHaveLength(1)
|
||||
|
||||
eventCapture.cleanup()
|
||||
})
|
||||
|
||||
it("should not promote widget if input is not connected", () => {
|
||||
it('should not promote widget if input is not connected', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input", type: "number" }],
|
||||
inputs: [{ name: 'input', type: 'number' }]
|
||||
})
|
||||
|
||||
const { node } = createNodeWithWidget("Test Node")
|
||||
const { node } = createNodeWithWidget('Test Node')
|
||||
subgraph.add(node)
|
||||
|
||||
// Don't connect - just create SubgraphNode
|
||||
@@ -230,12 +261,12 @@ describe("SubgraphWidgetPromotion", () => {
|
||||
expect(subgraphNode.widgets).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle disconnection of promoted widget", () => {
|
||||
it('should handle disconnection of promoted widget', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "input", type: "number" }],
|
||||
inputs: [{ name: 'input', type: 'number' }]
|
||||
})
|
||||
|
||||
const { node } = createNodeWithWidget("Test Node")
|
||||
const { node } = createNodeWithWidget('Test Node')
|
||||
const subgraphNode = setupPromotedWidget(subgraph, node)
|
||||
expect(subgraphNode.widgets).toHaveLength(1)
|
||||
|
||||
@@ -247,14 +278,20 @@ describe("SubgraphWidgetPromotion", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Tooltip Promotion", () => {
|
||||
it("should preserve widget tooltip when promoting", () => {
|
||||
describe('Tooltip Promotion', () => {
|
||||
it('should preserve widget tooltip when promoting', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "value", type: "number" }],
|
||||
inputs: [{ name: 'value', type: 'number' }]
|
||||
})
|
||||
|
||||
const originalTooltip = "This is a test tooltip"
|
||||
const { node } = createNodeWithWidget("Test Node", "number", 42, "number", originalTooltip)
|
||||
const originalTooltip = 'This is a test tooltip'
|
||||
const { node } = createNodeWithWidget(
|
||||
'Test Node',
|
||||
'number',
|
||||
42,
|
||||
'number',
|
||||
originalTooltip
|
||||
)
|
||||
const subgraphNode = setupPromotedWidget(subgraph, node)
|
||||
|
||||
// The promoted widget should preserve the original tooltip
|
||||
@@ -262,12 +299,12 @@ describe("SubgraphWidgetPromotion", () => {
|
||||
expect(subgraphNode.widgets[0].tooltip).toBe(originalTooltip)
|
||||
})
|
||||
|
||||
it("should handle widgets with no tooltip", () => {
|
||||
it('should handle widgets with no tooltip', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "value", type: "number" }],
|
||||
inputs: [{ name: 'value', type: 'number' }]
|
||||
})
|
||||
|
||||
const { node } = createNodeWithWidget("Test Node", "number", 42, "number")
|
||||
const { node } = createNodeWithWidget('Test Node', 'number', 42, 'number')
|
||||
const subgraphNode = setupPromotedWidget(subgraph, node)
|
||||
|
||||
// The promoted widget should have undefined tooltip
|
||||
@@ -275,37 +312,37 @@ describe("SubgraphWidgetPromotion", () => {
|
||||
expect(subgraphNode.widgets[0].tooltip).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should preserve tooltips for multiple promoted widgets", () => {
|
||||
it('should preserve tooltips for multiple promoted widgets', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: "input1", type: "number" },
|
||||
{ name: "input2", type: "string" },
|
||||
],
|
||||
{ name: 'input1', type: 'number' },
|
||||
{ name: 'input2', type: 'string' }
|
||||
]
|
||||
})
|
||||
|
||||
// Create node with multiple widgets with different tooltips
|
||||
const multiWidgetNode = new LGraphNode("Multi Widget Node")
|
||||
const numInput = multiWidgetNode.addInput("num", "number")
|
||||
const strInput = multiWidgetNode.addInput("str", "string")
|
||||
const multiWidgetNode = new LGraphNode('Multi Widget Node')
|
||||
const numInput = multiWidgetNode.addInput('num', 'number')
|
||||
const strInput = multiWidgetNode.addInput('str', 'string')
|
||||
|
||||
const widget1 = new BaseWidget({
|
||||
name: "widget1",
|
||||
type: "number",
|
||||
name: 'widget1',
|
||||
type: 'number',
|
||||
value: 10,
|
||||
y: 0,
|
||||
options: {},
|
||||
node: multiWidgetNode,
|
||||
tooltip: "Number widget tooltip",
|
||||
tooltip: 'Number widget tooltip'
|
||||
})
|
||||
|
||||
const widget2 = new BaseWidget({
|
||||
name: "widget2",
|
||||
type: "string",
|
||||
value: "hello",
|
||||
name: 'widget2',
|
||||
type: 'string',
|
||||
value: 'hello',
|
||||
y: 40,
|
||||
options: {},
|
||||
node: multiWidgetNode,
|
||||
tooltip: "String widget tooltip",
|
||||
tooltip: 'String widget tooltip'
|
||||
})
|
||||
|
||||
multiWidgetNode.widgets = [widget1, widget2]
|
||||
@@ -314,25 +351,37 @@ describe("SubgraphWidgetPromotion", () => {
|
||||
subgraph.add(multiWidgetNode)
|
||||
|
||||
// Connect both inputs
|
||||
subgraph.inputNode.slots[0].connect(multiWidgetNode.inputs[0], multiWidgetNode)
|
||||
subgraph.inputNode.slots[1].connect(multiWidgetNode.inputs[1], multiWidgetNode)
|
||||
subgraph.inputNode.slots[0].connect(
|
||||
multiWidgetNode.inputs[0],
|
||||
multiWidgetNode
|
||||
)
|
||||
subgraph.inputNode.slots[1].connect(
|
||||
multiWidgetNode.inputs[1],
|
||||
multiWidgetNode
|
||||
)
|
||||
|
||||
// Create SubgraphNode
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Both widgets should preserve their tooltips
|
||||
expect(subgraphNode.widgets).toHaveLength(2)
|
||||
expect(subgraphNode.widgets[0].tooltip).toBe("Number widget tooltip")
|
||||
expect(subgraphNode.widgets[1].tooltip).toBe("String widget tooltip")
|
||||
expect(subgraphNode.widgets[0].tooltip).toBe('Number widget tooltip')
|
||||
expect(subgraphNode.widgets[1].tooltip).toBe('String widget tooltip')
|
||||
})
|
||||
|
||||
it("should preserve original tooltip after promotion", () => {
|
||||
it('should preserve original tooltip after promotion', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "value", type: "number" }],
|
||||
inputs: [{ name: 'value', type: 'number' }]
|
||||
})
|
||||
|
||||
const originalTooltip = "Original tooltip"
|
||||
const { node } = createNodeWithWidget("Test Node", "number", 42, "number", originalTooltip)
|
||||
const originalTooltip = 'Original tooltip'
|
||||
const { node } = createNodeWithWidget(
|
||||
'Test Node',
|
||||
'number',
|
||||
42,
|
||||
'number',
|
||||
originalTooltip
|
||||
)
|
||||
const subgraphNode = setupPromotedWidget(subgraph, node)
|
||||
|
||||
const promotedWidget = subgraphNode.widgets[0]
|
||||
@@ -341,8 +390,8 @@ describe("SubgraphWidgetPromotion", () => {
|
||||
expect(promotedWidget.tooltip).toBe(originalTooltip)
|
||||
|
||||
// The promoted widget should still function normally
|
||||
expect(promotedWidget.name).toBe("value") // Uses subgraph input name
|
||||
expect(promotedWidget.type).toBe("number")
|
||||
expect(promotedWidget.name).toBe('value') // Uses subgraph input name
|
||||
expect(promotedWidget.type).toBe('number')
|
||||
expect(promotedWidget.value).toBe(42)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CapturedEvent } from "./subgraphHelpers"
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import { expect } from "vitest"
|
||||
import type { CapturedEvent } from './subgraphHelpers'
|
||||
|
||||
/**
|
||||
* Extended captured event with additional metadata not in the base infrastructure
|
||||
@@ -17,7 +17,7 @@ export interface ExtendedCapturedEvent<T = unknown> extends CapturedEvent<T> {
|
||||
*/
|
||||
export function createExtendedEventCapture<T = unknown>(
|
||||
eventTarget: EventTarget,
|
||||
eventTypes: string[],
|
||||
eventTypes: string[]
|
||||
) {
|
||||
const capturedEvents: ExtendedCapturedEvent<T>[] = []
|
||||
const listeners: Array<() => void> = []
|
||||
@@ -30,7 +30,7 @@ export function createExtendedEventCapture<T = unknown>(
|
||||
timestamp: Date.now(),
|
||||
defaultPrevented: event.defaultPrevented,
|
||||
bubbles: event.bubbles,
|
||||
cancelable: event.cancelable,
|
||||
cancelable: event.cancelable
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,17 +40,25 @@ export function createExtendedEventCapture<T = unknown>(
|
||||
|
||||
return {
|
||||
events: capturedEvents,
|
||||
clear: () => { capturedEvents.length = 0 },
|
||||
cleanup: () => { for (const cleanup of listeners) cleanup() },
|
||||
getEventsByType: (type: string) => capturedEvents.filter(e => e.type === type),
|
||||
clear: () => {
|
||||
capturedEvents.length = 0
|
||||
},
|
||||
cleanup: () => {
|
||||
for (const cleanup of listeners) cleanup()
|
||||
},
|
||||
getEventsByType: (type: string) =>
|
||||
capturedEvents.filter((e) => e.type === type),
|
||||
getLatestEvent: () => capturedEvents.at(-1),
|
||||
getFirstEvent: () => capturedEvents[0],
|
||||
|
||||
/**
|
||||
* Wait for a specific event type to be captured
|
||||
*/
|
||||
async waitForEvent(type: string, timeoutMs: number = 1000): Promise<ExtendedCapturedEvent<T>> {
|
||||
const existingEvent = capturedEvents.find(e => e.type === type)
|
||||
async waitForEvent(
|
||||
type: string,
|
||||
timeoutMs: number = 1000
|
||||
): Promise<ExtendedCapturedEvent<T>> {
|
||||
const existingEvent = capturedEvents.find((e) => e.type === type)
|
||||
if (existingEvent) return existingEvent
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -60,7 +68,7 @@ export function createExtendedEventCapture<T = unknown>(
|
||||
}, timeoutMs)
|
||||
|
||||
const eventListener = (_event: Event) => {
|
||||
const capturedEvent = capturedEvents.find(e => e.type === type)
|
||||
const capturedEvent = capturedEvents.find((e) => e.type === type)
|
||||
if (capturedEvent) {
|
||||
clearTimeout(timeout)
|
||||
eventTarget.removeEventListener(type, eventListener)
|
||||
@@ -75,11 +83,18 @@ export function createExtendedEventCapture<T = unknown>(
|
||||
/**
|
||||
* Wait for a sequence of events to occur in order
|
||||
*/
|
||||
async waitForSequence(expectedSequence: string[], timeoutMs: number = 1000): Promise<ExtendedCapturedEvent<T>[]> {
|
||||
async waitForSequence(
|
||||
expectedSequence: string[],
|
||||
timeoutMs: number = 1000
|
||||
): Promise<ExtendedCapturedEvent<T>[]> {
|
||||
// Check if sequence is already complete
|
||||
if (capturedEvents.length >= expectedSequence.length) {
|
||||
const actualSequence = capturedEvents.slice(0, expectedSequence.length).map(e => e.type)
|
||||
if (JSON.stringify(actualSequence) === JSON.stringify(expectedSequence)) {
|
||||
const actualSequence = capturedEvents
|
||||
.slice(0, expectedSequence.length)
|
||||
.map((e) => e.type)
|
||||
if (
|
||||
JSON.stringify(actualSequence) === JSON.stringify(expectedSequence)
|
||||
) {
|
||||
return capturedEvents.slice(0, expectedSequence.length)
|
||||
}
|
||||
}
|
||||
@@ -87,15 +102,24 @@ export function createExtendedEventCapture<T = unknown>(
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup()
|
||||
const actual = capturedEvents.map(e => e.type).join(", ")
|
||||
const expected = expectedSequence.join(", ")
|
||||
reject(new Error(`Event sequence not completed within ${timeoutMs}ms. Expected: ${expected}, Got: ${actual}`))
|
||||
const actual = capturedEvents.map((e) => e.type).join(', ')
|
||||
const expected = expectedSequence.join(', ')
|
||||
reject(
|
||||
new Error(
|
||||
`Event sequence not completed within ${timeoutMs}ms. Expected: ${expected}, Got: ${actual}`
|
||||
)
|
||||
)
|
||||
}, timeoutMs)
|
||||
|
||||
const checkSequence = () => {
|
||||
if (capturedEvents.length >= expectedSequence.length) {
|
||||
const actualSequence = capturedEvents.slice(0, expectedSequence.length).map(e => e.type)
|
||||
if (JSON.stringify(actualSequence) === JSON.stringify(expectedSequence)) {
|
||||
const actualSequence = capturedEvents
|
||||
.slice(0, expectedSequence.length)
|
||||
.map((e) => e.type)
|
||||
if (
|
||||
JSON.stringify(actualSequence) ===
|
||||
JSON.stringify(expectedSequence)
|
||||
) {
|
||||
cleanup()
|
||||
resolve(capturedEvents.slice(0, expectedSequence.length))
|
||||
}
|
||||
@@ -119,7 +143,7 @@ export function createExtendedEventCapture<T = unknown>(
|
||||
// Initial check in case events already exist
|
||||
checkSequence()
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,14 +162,14 @@ export interface MemoryLeakTestOptions {
|
||||
* Useful for testing that event listeners and references are properly cleaned up
|
||||
*/
|
||||
export function createMemoryLeakTest<T>(
|
||||
setupFn: () => { ref: WeakRef<T>, cleanup: () => void },
|
||||
options: MemoryLeakTestOptions = {},
|
||||
setupFn: () => { ref: WeakRef<T>; cleanup: () => void },
|
||||
options: MemoryLeakTestOptions = {}
|
||||
) {
|
||||
const {
|
||||
cycles = 1,
|
||||
instancesPerCycle = 1,
|
||||
gcAfterEach = true,
|
||||
maxMemoryGrowth = 0,
|
||||
maxMemoryGrowth = 0
|
||||
} = options
|
||||
|
||||
return async () => {
|
||||
@@ -165,19 +189,21 @@ export function createMemoryLeakTest<T>(
|
||||
|
||||
if (gcAfterEach && global.gc) {
|
||||
global.gc()
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
}
|
||||
}
|
||||
|
||||
// Final garbage collection
|
||||
if (global.gc) {
|
||||
global.gc()
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Check if objects were collected
|
||||
const uncollectedRefs = refs.filter(ref => ref.deref() !== undefined)
|
||||
const uncollectedRefs = refs.filter((ref) => ref.deref() !== undefined)
|
||||
if (uncollectedRefs.length > 0) {
|
||||
console.warn(`${uncollectedRefs.length} objects were not garbage collected`)
|
||||
console.warn(
|
||||
`${uncollectedRefs.length} objects were not garbage collected`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +213,9 @@ export function createMemoryLeakTest<T>(
|
||||
const memoryGrowth = finalMemory - initialMemory
|
||||
|
||||
if (memoryGrowth > maxMemoryGrowth) {
|
||||
throw new Error(`Memory growth ${memoryGrowth} bytes exceeds limit ${maxMemoryGrowth} bytes`)
|
||||
throw new Error(
|
||||
`Memory growth ${memoryGrowth} bytes exceeds limit ${maxMemoryGrowth} bytes`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +242,7 @@ export function createEventPerformanceMonitor() {
|
||||
measurements.push({
|
||||
operation,
|
||||
duration: end - start,
|
||||
timestamp: start,
|
||||
timestamp: start
|
||||
})
|
||||
|
||||
return result
|
||||
@@ -223,22 +251,33 @@ export function createEventPerformanceMonitor() {
|
||||
getMeasurements: () => [...measurements],
|
||||
|
||||
getAverageDuration: (operation: string) => {
|
||||
const operationMeasurements = measurements.filter(m => m.operation === operation)
|
||||
const operationMeasurements = measurements.filter(
|
||||
(m) => m.operation === operation
|
||||
)
|
||||
if (operationMeasurements.length === 0) return 0
|
||||
|
||||
const totalDuration = operationMeasurements.reduce((sum, m) => sum + m.duration, 0)
|
||||
const totalDuration = operationMeasurements.reduce(
|
||||
(sum, m) => sum + m.duration,
|
||||
0
|
||||
)
|
||||
return totalDuration / operationMeasurements.length
|
||||
},
|
||||
|
||||
clear: () => { measurements.length = 0 },
|
||||
clear: () => {
|
||||
measurements.length = 0
|
||||
},
|
||||
|
||||
assertPerformance: (operation: string, maxDuration: number) => {
|
||||
const measurements = this.getMeasurements()
|
||||
const relevantMeasurements = measurements.filter(m => m.operation === operation)
|
||||
const relevantMeasurements = measurements.filter(
|
||||
(m) => m.operation === operation
|
||||
)
|
||||
if (relevantMeasurements.length === 0) return
|
||||
|
||||
const avgDuration = relevantMeasurements.reduce((sum, m) => sum + m.duration, 0) / relevantMeasurements.length
|
||||
const avgDuration =
|
||||
relevantMeasurements.reduce((sum, m) => sum + m.duration, 0) /
|
||||
relevantMeasurements.length
|
||||
expect(avgDuration).toBeLessThan(maxDuration)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,16 @@
|
||||
* in their test files. Each fixture provides a clean, pre-configured subgraph
|
||||
* setup for different testing scenarios.
|
||||
*/
|
||||
import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
|
||||
import { LGraph, Subgraph } from "@/lib/litegraph/src/litegraph"
|
||||
import { SubgraphNode } from "@/lib/litegraph/src/subgraph/SubgraphNode"
|
||||
|
||||
import { test } from "../../testExtensions"
|
||||
import { test } from '../../testExtensions'
|
||||
import {
|
||||
createEventCapture,
|
||||
createNestedSubgraphs,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
} from "./subgraphHelpers"
|
||||
createTestSubgraphNode
|
||||
} from './subgraphHelpers'
|
||||
|
||||
export interface SubgraphFixtures {
|
||||
/** A minimal subgraph with no inputs, outputs, or nodes */
|
||||
@@ -58,72 +57,71 @@ export interface SubgraphFixtures {
|
||||
* ```
|
||||
*/
|
||||
export const subgraphTest = test.extend<SubgraphFixtures>({
|
||||
|
||||
emptySubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
emptySubgraph: async ({}, use: (value: unknown) => Promise<void>) => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Empty Test Subgraph",
|
||||
name: 'Empty Test Subgraph',
|
||||
inputCount: 0,
|
||||
outputCount: 0,
|
||||
nodeCount: 0,
|
||||
nodeCount: 0
|
||||
})
|
||||
|
||||
await use(subgraph)
|
||||
},
|
||||
|
||||
simpleSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
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,
|
||||
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>) => {
|
||||
complexSubgraph: async ({}, use: (value: unknown) => Promise<void>) => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Complex Test Subgraph",
|
||||
name: 'Complex Test Subgraph',
|
||||
inputs: [
|
||||
{ name: "data", type: "number" },
|
||||
{ name: "control", type: "boolean" },
|
||||
{ name: "text", type: "string" },
|
||||
{ name: 'data', type: 'number' },
|
||||
{ name: 'control', type: 'boolean' },
|
||||
{ name: 'text', type: 'string' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "result", type: "number" },
|
||||
{ name: "status", type: "boolean" },
|
||||
{ name: 'result', type: 'number' },
|
||||
{ name: 'status', type: 'boolean' }
|
||||
],
|
||||
nodeCount: 5,
|
||||
nodeCount: 5
|
||||
})
|
||||
|
||||
await use(subgraph)
|
||||
},
|
||||
|
||||
nestedSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
nestedSubgraph: async ({}, use: (value: unknown) => Promise<void>) => {
|
||||
const nested = createNestedSubgraphs({
|
||||
depth: 3,
|
||||
nodesPerLevel: 2,
|
||||
inputsPerSubgraph: 1,
|
||||
outputsPerSubgraph: 1,
|
||||
outputsPerSubgraph: 1
|
||||
})
|
||||
|
||||
await use(nested)
|
||||
},
|
||||
|
||||
subgraphWithNode: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
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,
|
||||
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],
|
||||
size: [180, 80]
|
||||
})
|
||||
|
||||
// Add the subgraph node to the parent graph
|
||||
@@ -132,32 +130,32 @@ export const subgraphTest = test.extend<SubgraphFixtures>({
|
||||
await use({
|
||||
subgraph,
|
||||
subgraphNode,
|
||||
parentGraph,
|
||||
parentGraph
|
||||
})
|
||||
},
|
||||
|
||||
eventCapture: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
eventCapture: async ({}, use: (value: unknown) => Promise<void>) => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Event Test Subgraph",
|
||||
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",
|
||||
'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()
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -186,21 +184,20 @@ export interface EdgeCaseFixtures {
|
||||
* These tests may intentionally create invalid states.
|
||||
*/
|
||||
export const edgeCaseTest = subgraphTest.extend<EdgeCaseFixtures>({
|
||||
|
||||
circularSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
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: "*" }],
|
||||
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: "*" }],
|
||||
name: 'Subgraph B',
|
||||
inputs: [{ name: 'input', type: '*' }],
|
||||
outputs: [{ name: 'output', type: '*' }]
|
||||
})
|
||||
|
||||
// Create instances (this doesn't create circular refs by itself)
|
||||
@@ -216,43 +213,43 @@ export const edgeCaseTest = subgraphTest.extend<EdgeCaseFixtures>({
|
||||
subgraphA,
|
||||
subgraphB,
|
||||
nodeA,
|
||||
nodeB,
|
||||
nodeB
|
||||
})
|
||||
},
|
||||
|
||||
deeplyNestedSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
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,
|
||||
outputsPerSubgraph: 1
|
||||
})
|
||||
|
||||
await use(nested)
|
||||
},
|
||||
|
||||
maxIOSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
|
||||
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,
|
||||
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,
|
||||
type: i % 2 === 0 ? 'number' : ('string' as const)
|
||||
}))
|
||||
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Max IO Subgraph",
|
||||
name: 'Max IO Subgraph',
|
||||
inputs,
|
||||
outputs,
|
||||
nodeCount: 10,
|
||||
nodeCount: 10
|
||||
})
|
||||
|
||||
await use(subgraph)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -261,7 +258,7 @@ export const edgeCaseTest = subgraphTest.extend<EdgeCaseFixtures>({
|
||||
*/
|
||||
export function verifyFixtureIntegrity<T extends Record<string, unknown>>(
|
||||
fixture: T,
|
||||
expectedProperties: (keyof T)[],
|
||||
expectedProperties: (keyof T)[]
|
||||
): void {
|
||||
for (const prop of expectedProperties) {
|
||||
if (!(prop in fixture)) {
|
||||
@@ -285,9 +282,9 @@ export function createSubgraphSnapshot(subgraph: Subgraph) {
|
||||
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 })),
|
||||
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,
|
||||
hasOutputNode: !!subgraph.outputNode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,17 @@
|
||||
* These functions provide consistent ways to create test subgraphs, nodes, and
|
||||
* verify their behavior.
|
||||
*/
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import type { ISlotType, NodeId } from "@/lib/litegraph/src/litegraph"
|
||||
import type { ExportedSubgraph, ExportedSubgraphInstance } from "@/lib/litegraph/src/types/serialisation"
|
||||
import type { UUID } from "@/lib/litegraph/src/utils/uuid"
|
||||
|
||||
import { expect } from "vitest"
|
||||
|
||||
import { LGraph, LGraphNode, Subgraph } from "@/lib/litegraph/src/litegraph"
|
||||
import { SubgraphNode } from "@/lib/litegraph/src/subgraph/SubgraphNode"
|
||||
import { createUuidv4 } from "@/lib/litegraph/src/utils/uuid"
|
||||
import type { ISlotType, NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraph, LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type {
|
||||
ExportedSubgraph,
|
||||
ExportedSubgraphInstance
|
||||
} from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { createUuidv4 } from '@/lib/litegraph/src/utils/uuid'
|
||||
|
||||
export interface TestSubgraphOptions {
|
||||
id?: UUID
|
||||
@@ -22,8 +23,8 @@ export interface TestSubgraphOptions {
|
||||
nodeCount?: number
|
||||
inputCount?: number
|
||||
outputCount?: number
|
||||
inputs?: Array<{ name: string, type: ISlotType }>
|
||||
outputs?: Array<{ name: string, type: ISlotType }>
|
||||
inputs?: Array<{ name: string; type: ISlotType }>
|
||||
outputs?: Array<{ name: string; type: ISlotType }>
|
||||
}
|
||||
|
||||
export interface TestSubgraphNodeOptions {
|
||||
@@ -72,15 +73,21 @@ export interface CapturedEvent<T = unknown> {
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph {
|
||||
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)}`)
|
||||
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)}`)
|
||||
throw new Error(
|
||||
`Cannot specify both 'outputs' array and 'outputCount'. Choose one approach. Received options: ${JSON.stringify(options)}`
|
||||
)
|
||||
}
|
||||
const rootGraph = new LGraph()
|
||||
|
||||
@@ -96,24 +103,24 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph
|
||||
|
||||
// Subgraph-specific properties
|
||||
id: options.id || createUuidv4(),
|
||||
name: options.name || "Test Subgraph",
|
||||
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,
|
||||
pinned: false
|
||||
},
|
||||
outputNode: {
|
||||
id: -20, // SUBGRAPH_OUTPUT_ID
|
||||
bounding: [400, 100, 140, 126], // [x, y, width, height]
|
||||
pinned: false,
|
||||
pinned: false
|
||||
},
|
||||
|
||||
// IO definitions - will be populated by addInput/addOutput calls
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [],
|
||||
widgets: []
|
||||
}
|
||||
|
||||
// Create the subgraph
|
||||
@@ -126,7 +133,7 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph
|
||||
}
|
||||
} else if (options.inputCount) {
|
||||
for (let i = 0; i < options.inputCount; i++) {
|
||||
subgraph.addInput(`input_${i}`, "*")
|
||||
subgraph.addInput(`input_${i}`, '*')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +144,7 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph
|
||||
}
|
||||
} else if (options.outputCount) {
|
||||
for (let i = 0; i < options.outputCount; i++) {
|
||||
subgraph.addOutput(`output_${i}`, "*")
|
||||
subgraph.addOutput(`output_${i}`, '*')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,8 +152,8 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph
|
||||
if (options.nodeCount) {
|
||||
for (let i = 0; i < options.nodeCount; i++) {
|
||||
const node = new LGraphNode(`Test Node ${i}`)
|
||||
node.addInput("in", "*")
|
||||
node.addOutput("out", "*")
|
||||
node.addInput('in', '*')
|
||||
node.addOutput('out', '*')
|
||||
subgraph.add(node)
|
||||
}
|
||||
}
|
||||
@@ -172,7 +179,7 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph
|
||||
*/
|
||||
export function createTestSubgraphNode(
|
||||
subgraph: Subgraph,
|
||||
options: TestSubgraphNodeOptions = {},
|
||||
options: TestSubgraphNodeOptions = {}
|
||||
): SubgraphNode {
|
||||
const parentGraph = new LGraph()
|
||||
|
||||
@@ -185,7 +192,7 @@ export function createTestSubgraphNode(
|
||||
outputs: [],
|
||||
properties: {},
|
||||
flags: {},
|
||||
mode: 0,
|
||||
mode: 0
|
||||
}
|
||||
|
||||
return new SubgraphNode(parentGraph, subgraph, instanceData)
|
||||
@@ -206,7 +213,7 @@ export function createNestedSubgraphs(options: NestedSubgraphOptions = {}) {
|
||||
depth = 2,
|
||||
nodesPerLevel = 2,
|
||||
inputsPerSubgraph = 1,
|
||||
outputsPerSubgraph = 1,
|
||||
outputsPerSubgraph = 1
|
||||
} = options
|
||||
|
||||
const rootGraph = new LGraph()
|
||||
@@ -221,14 +228,14 @@ export function createNestedSubgraphs(options: NestedSubgraphOptions = {}) {
|
||||
name: `Level ${level} Subgraph`,
|
||||
nodeCount: nodesPerLevel,
|
||||
inputCount: inputsPerSubgraph,
|
||||
outputCount: outputsPerSubgraph,
|
||||
outputCount: outputsPerSubgraph
|
||||
})
|
||||
|
||||
subgraphs.push(subgraph)
|
||||
|
||||
// Create instance in parent
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, {
|
||||
pos: [100 + level * 200, 100],
|
||||
pos: [100 + level * 200, 100]
|
||||
})
|
||||
|
||||
if (currentParent instanceof LGraph) {
|
||||
@@ -248,7 +255,7 @@ export function createNestedSubgraphs(options: NestedSubgraphOptions = {}) {
|
||||
subgraphs,
|
||||
subgraphNodes,
|
||||
depth,
|
||||
leafSubgraph: subgraphs.at(-1),
|
||||
leafSubgraph: subgraphs.at(-1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +275,7 @@ export function createNestedSubgraphs(options: NestedSubgraphOptions = {}) {
|
||||
*/
|
||||
export function assertSubgraphStructure(
|
||||
subgraph: Subgraph,
|
||||
expected: SubgraphStructureExpectation,
|
||||
expected: SubgraphStructureExpectation
|
||||
): void {
|
||||
if (expected.inputCount !== undefined) {
|
||||
expect(subgraph.inputs.length).toBe(expected.inputCount)
|
||||
@@ -314,7 +321,7 @@ export function assertSubgraphStructure(
|
||||
*/
|
||||
export function verifyEventSequence<T = unknown>(
|
||||
capturedEvents: CapturedEvent<T>[],
|
||||
expectedSequence: string[],
|
||||
expectedSequence: string[]
|
||||
): void {
|
||||
expect(capturedEvents.length).toBe(expectedSequence.length)
|
||||
|
||||
@@ -325,7 +332,7 @@ export function verifyEventSequence<T = unknown>(
|
||||
// Verify timestamps are in order
|
||||
for (let i = 1; i < capturedEvents.length; i++) {
|
||||
expect(capturedEvents[i].timestamp).toBeGreaterThanOrEqual(
|
||||
capturedEvents[i - 1].timestamp,
|
||||
capturedEvents[i - 1].timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -336,7 +343,9 @@ export function verifyEventSequence<T = unknown>(
|
||||
* @param overrides Properties to override in the default data
|
||||
* @returns ExportedSubgraph data structure
|
||||
*/
|
||||
export function createTestSubgraphData(overrides: Partial<ExportedSubgraph> = {}): ExportedSubgraph {
|
||||
export function createTestSubgraphData(
|
||||
overrides: Partial<ExportedSubgraph> = {}
|
||||
): ExportedSubgraph {
|
||||
return {
|
||||
version: 1,
|
||||
nodes: [],
|
||||
@@ -346,24 +355,24 @@ export function createTestSubgraphData(overrides: Partial<ExportedSubgraph> = {}
|
||||
definitions: { subgraphs: [] },
|
||||
|
||||
id: createUuidv4(),
|
||||
name: "Test Data Subgraph",
|
||||
name: 'Test Data Subgraph',
|
||||
|
||||
inputNode: {
|
||||
id: -10,
|
||||
bounding: [10, 100, 150, 126],
|
||||
pinned: false,
|
||||
pinned: false
|
||||
},
|
||||
outputNode: {
|
||||
id: -20,
|
||||
bounding: [400, 100, 140, 126],
|
||||
pinned: false,
|
||||
pinned: false
|
||||
},
|
||||
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [],
|
||||
|
||||
...overrides,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,29 +382,34 @@ export function createTestSubgraphData(overrides: Partial<ExportedSubgraph> = {}
|
||||
* @param nodeCount Number of internal nodes to create
|
||||
* @returns Complex subgraph data structure
|
||||
*/
|
||||
export function createComplexSubgraphData(nodeCount: number = 5): ExportedSubgraph {
|
||||
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
|
||||
}> = {}
|
||||
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",
|
||||
type: 'basic/test',
|
||||
pos: [100 + i * 150, 200],
|
||||
size: [120, 60],
|
||||
inputs: [{ name: "in", type: "*", link: null }],
|
||||
outputs: [{ name: "out", type: "*", links: [] }],
|
||||
inputs: [{ name: 'in', type: '*', link: null }],
|
||||
outputs: [{ name: 'out', type: '*', links: [] }],
|
||||
properties: { value: i },
|
||||
flags: {},
|
||||
mode: 0,
|
||||
mode: 0
|
||||
})
|
||||
}
|
||||
|
||||
@@ -408,7 +422,7 @@ export function createComplexSubgraphData(nodeCount: number = 5): ExportedSubgra
|
||||
origin_slot: 0,
|
||||
target_id: i + 2,
|
||||
target_slot: 0,
|
||||
type: "*",
|
||||
type: '*'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,13 +430,13 @@ export function createComplexSubgraphData(nodeCount: number = 5): ExportedSubgra
|
||||
nodes,
|
||||
links,
|
||||
inputs: [
|
||||
{ name: "input1", type: "number", pos: [0, 0] },
|
||||
{ name: "input2", type: "string", pos: [0, 1] },
|
||||
{ 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] },
|
||||
],
|
||||
{ name: 'output1', type: 'number', pos: [0, 0] },
|
||||
{ name: 'output2', type: 'string', pos: [0, 1] }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -434,7 +448,7 @@ export function createComplexSubgraphData(nodeCount: number = 5): ExportedSubgra
|
||||
*/
|
||||
export function createEventCapture<T = unknown>(
|
||||
eventTarget: EventTarget,
|
||||
eventTypes: string[],
|
||||
eventTypes: string[]
|
||||
) {
|
||||
const capturedEvents: CapturedEvent<T>[] = []
|
||||
const listeners: Array<() => void> = []
|
||||
@@ -445,7 +459,7 @@ export function createEventCapture<T = unknown>(
|
||||
capturedEvents.push({
|
||||
type: eventType,
|
||||
detail: (event as CustomEvent<T>).detail,
|
||||
timestamp: Date.now(),
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -455,12 +469,15 @@ export function createEventCapture<T = unknown>(
|
||||
|
||||
return {
|
||||
events: capturedEvents,
|
||||
clear: () => { capturedEvents.length = 0 },
|
||||
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),
|
||||
getEventsByType: (type: string) =>
|
||||
capturedEvents.filter((e) => e.type === type)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,7 +486,10 @@ export function createEventCapture<T = unknown>(
|
||||
* @param subgraph The subgraph to inspect
|
||||
* @param label Optional label for the log output
|
||||
*/
|
||||
export function logSubgraphStructure(subgraph: Subgraph, label: string = "Subgraph"): void {
|
||||
export function logSubgraphStructure(
|
||||
subgraph: Subgraph,
|
||||
label: string = 'Subgraph'
|
||||
): void {
|
||||
console.log(`\n=== ${label} Structure ===`)
|
||||
console.log(`Name: ${subgraph.name}`)
|
||||
console.log(`ID: ${subgraph.id}`)
|
||||
@@ -479,15 +499,21 @@ export function logSubgraphStructure(subgraph: Subgraph, label: string = "Subgra
|
||||
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 })))
|
||||
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(
|
||||
'Output details:',
|
||||
subgraph.outputs.map((o) => ({ name: o.name, type: o.type }))
|
||||
)
|
||||
}
|
||||
|
||||
console.log("========================\n")
|
||||
console.log('========================\n')
|
||||
}
|
||||
|
||||
// Re-export expect from vitest for convenience
|
||||
export { expect } from "vitest"
|
||||
export { expect } from 'vitest'
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import type { UUID } from "@/lib/litegraph/src/utils/uuid"
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { LGraph } from "@/lib/litegraph/src/litegraph"
|
||||
import { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
findUsedSubgraphIds,
|
||||
getDirectSubgraphIds,
|
||||
} from "@/lib/litegraph/src/subgraph/subgraphUtils"
|
||||
getDirectSubgraphIds
|
||||
} from '@/lib/litegraph/src/subgraph/subgraphUtils'
|
||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
|
||||
import { createTestSubgraph, createTestSubgraphNode } from "./fixtures/subgraphHelpers"
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
describe("subgraphUtils", () => {
|
||||
describe("getDirectSubgraphIds", () => {
|
||||
it("should return empty set for graph with no subgraph nodes", () => {
|
||||
describe('subgraphUtils', () => {
|
||||
describe('getDirectSubgraphIds', () => {
|
||||
it('should return empty set for graph with no subgraph nodes', () => {
|
||||
const graph = new LGraph()
|
||||
const result = getDirectSubgraphIds(graph)
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
|
||||
it("should find single subgraph node", () => {
|
||||
it('should find single subgraph node', () => {
|
||||
const graph = new LGraph()
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -29,10 +31,10 @@ describe("subgraphUtils", () => {
|
||||
expect(result.has(subgraph.id)).toBe(true)
|
||||
})
|
||||
|
||||
it("should find multiple unique subgraph nodes", () => {
|
||||
it('should find multiple unique subgraph nodes', () => {
|
||||
const graph = new LGraph()
|
||||
const subgraph1 = createTestSubgraph({ name: "Subgraph 1" })
|
||||
const subgraph2 = createTestSubgraph({ name: "Subgraph 2" })
|
||||
const subgraph1 = createTestSubgraph({ name: 'Subgraph 1' })
|
||||
const subgraph2 = createTestSubgraph({ name: 'Subgraph 2' })
|
||||
|
||||
const node1 = createTestSubgraphNode(subgraph1)
|
||||
const node2 = createTestSubgraphNode(subgraph2)
|
||||
@@ -46,7 +48,7 @@ describe("subgraphUtils", () => {
|
||||
expect(result.has(subgraph2.id)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return unique IDs when same subgraph is used multiple times", () => {
|
||||
it('should return unique IDs when same subgraph is used multiple times', () => {
|
||||
const graph = new LGraph()
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
@@ -62,8 +64,8 @@ describe("subgraphUtils", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("findUsedSubgraphIds", () => {
|
||||
it("should handle graph with no subgraphs", () => {
|
||||
describe('findUsedSubgraphIds', () => {
|
||||
it('should handle graph with no subgraphs', () => {
|
||||
const graph = new LGraph()
|
||||
const registry = new Map<UUID, any>()
|
||||
|
||||
@@ -71,10 +73,10 @@ describe("subgraphUtils", () => {
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
|
||||
it("should find nested subgraphs", () => {
|
||||
it('should find nested subgraphs', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraph1 = createTestSubgraph({ name: "Level 1" })
|
||||
const subgraph2 = createTestSubgraph({ name: "Level 2" })
|
||||
const subgraph1 = createTestSubgraph({ name: 'Level 1' })
|
||||
const subgraph2 = createTestSubgraph({ name: 'Level 2' })
|
||||
|
||||
// Add subgraph1 node to root
|
||||
const node1 = createTestSubgraphNode(subgraph1)
|
||||
@@ -86,7 +88,7 @@ describe("subgraphUtils", () => {
|
||||
|
||||
const registry = new Map<UUID, any>([
|
||||
[subgraph1.id, subgraph1],
|
||||
[subgraph2.id, subgraph2],
|
||||
[subgraph2.id, subgraph2]
|
||||
])
|
||||
|
||||
const result = findUsedSubgraphIds(rootGraph, registry)
|
||||
@@ -95,10 +97,10 @@ describe("subgraphUtils", () => {
|
||||
expect(result.has(subgraph2.id)).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle circular references without infinite loop", () => {
|
||||
it('should handle circular references without infinite loop', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraph1 = createTestSubgraph({ name: "Subgraph 1" })
|
||||
const subgraph2 = createTestSubgraph({ name: "Subgraph 2" })
|
||||
const subgraph1 = createTestSubgraph({ name: 'Subgraph 1' })
|
||||
const subgraph2 = createTestSubgraph({ name: 'Subgraph 2' })
|
||||
|
||||
// Add subgraph1 to root
|
||||
const node1 = createTestSubgraphNode(subgraph1)
|
||||
@@ -114,7 +116,7 @@ describe("subgraphUtils", () => {
|
||||
|
||||
const registry = new Map<UUID, any>([
|
||||
[subgraph1.id, subgraph1],
|
||||
[subgraph2.id, subgraph2],
|
||||
[subgraph2.id, subgraph2]
|
||||
])
|
||||
|
||||
const result = findUsedSubgraphIds(rootGraph, registry)
|
||||
@@ -123,10 +125,10 @@ describe("subgraphUtils", () => {
|
||||
expect(result.has(subgraph2.id)).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle missing subgraphs in registry gracefully", () => {
|
||||
it('should handle missing subgraphs in registry gracefully', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraph1 = createTestSubgraph({ name: "Subgraph 1" })
|
||||
const subgraph2 = createTestSubgraph({ name: "Subgraph 2" })
|
||||
const subgraph1 = createTestSubgraph({ name: 'Subgraph 1' })
|
||||
const subgraph2 = createTestSubgraph({ name: 'Subgraph 2' })
|
||||
|
||||
// Add both subgraph nodes
|
||||
const node1 = createTestSubgraphNode(subgraph1)
|
||||
|
||||
Reference in New Issue
Block a user