mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 06:19:58 +00:00
[test] Add subgraph units tests for events and i/o (#1126)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -448,7 +448,7 @@ describe("ExecutableNodeDTO Scale Testing", () => {
|
||||
{ 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: 10, expectedId: "1:2:3:4:5:6:7:8:9:10:999" },
|
||||
]
|
||||
|
||||
for (const testCase of testCases) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Core Subgraph Tests
|
||||
*
|
||||
* Fundamental tests for the Subgraph class covering construction,
|
||||
* basic I/O management, and edge cases.
|
||||
* This file implements fundamental tests for the Subgraph class that establish
|
||||
* 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"
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
assertSubgraphStructure,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphData,
|
||||
verifyEventSequence,
|
||||
} from "./fixtures/subgraphHelpers"
|
||||
|
||||
describe("Subgraph Construction", () => {
|
||||
@@ -154,87 +154,6 @@ describe("Subgraph Input/Output Management", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Subgraph Event System", () => {
|
||||
subgraphTest("should fire events when adding inputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addInput("test_input", "number")
|
||||
|
||||
verifyEventSequence(capture.events, ["adding-input", "input-added"])
|
||||
|
||||
expect(capture.events[0].detail.name).toBe("test_input")
|
||||
expect(capture.events[0].detail.type).toBe("number")
|
||||
expect(capture.events[1].detail.input.name).toBe("test_input")
|
||||
})
|
||||
|
||||
subgraphTest("should fire events when adding outputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addOutput("test_output", "string")
|
||||
|
||||
verifyEventSequence(capture.events, ["adding-output", "output-added"])
|
||||
|
||||
expect(capture.events[0].detail.name).toBe("test_output")
|
||||
expect(capture.events[0].detail.type).toBe("string")
|
||||
expect(capture.events[1].detail.output.name).toBe("test_output")
|
||||
})
|
||||
|
||||
subgraphTest("should fire events when removing inputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
// Add an input first
|
||||
const input = subgraph.addInput("test_input", "number")
|
||||
capture.clear() // Clear the add events
|
||||
|
||||
// Remove the input
|
||||
subgraph.removeInput(input)
|
||||
|
||||
verifyEventSequence(capture.events, ["removing-input"])
|
||||
expect(capture.events[0].detail.input.name).toBe("test_input")
|
||||
expect(capture.events[0].detail.index).toBe(0)
|
||||
})
|
||||
|
||||
subgraphTest("should fire events when removing outputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
// Add an output first
|
||||
const output = subgraph.addOutput("test_output", "string")
|
||||
capture.clear() // Clear the add events
|
||||
|
||||
// Remove the output
|
||||
subgraph.removeOutput(output)
|
||||
|
||||
verifyEventSequence(capture.events, ["removing-output"])
|
||||
expect(capture.events[0].detail.output.name).toBe("test_output")
|
||||
expect(capture.events[0].detail.index).toBe(0)
|
||||
})
|
||||
|
||||
subgraphTest("should allow preventing input removal via event", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
// Add an input
|
||||
const input = subgraph.addInput("protected_input", "number")
|
||||
|
||||
// Add event listener that prevents removal
|
||||
subgraph.events.addEventListener("removing-input", (event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
capture.clear()
|
||||
|
||||
// Try to remove the input
|
||||
subgraph.removeInput(input)
|
||||
|
||||
// Input should still exist
|
||||
expect(subgraph.inputs).toHaveLength(1)
|
||||
expect(subgraph.inputs[0].name).toBe("protected_input")
|
||||
|
||||
// Event should have been fired but removal prevented
|
||||
expect(capture.events).toHaveLength(1)
|
||||
expect(capture.events[0].type).toBe("removing-input")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Subgraph Serialization", () => {
|
||||
subgraphTest("should serialize empty subgraph", ({ emptySubgraph }) => {
|
||||
const serialized = emptySubgraph.asSerialisable()
|
||||
@@ -270,6 +189,26 @@ describe("Subgraph Serialization", () => {
|
||||
})
|
||||
|
||||
describe("Subgraph Known Issues", () => {
|
||||
it.todo("should document createNode() bug returns null", () => {
|
||||
// This test documents the known issue where LiteGraph.createNode(subgraph.id)
|
||||
// returns null because UUID is not registered as a node type.
|
||||
//
|
||||
// Expected behavior: Should create a SubgraphNode instance
|
||||
// Actual behavior: Returns null, causing convertToSubgraph() to fail
|
||||
//
|
||||
// This needs to be fixed in the LiteGraphGlobal registration system.
|
||||
})
|
||||
|
||||
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.
|
||||
//
|
||||
// Expected behavior: Should throw error when nesting exceeds limit
|
||||
// Actual behavior: No validation is performed
|
||||
//
|
||||
// This safety limit should be implemented to prevent runaway recursion.
|
||||
})
|
||||
|
||||
it("should provide MAX_NESTED_SUBGRAPHS constant", () => {
|
||||
expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBe(1000)
|
||||
})
|
||||
|
||||
458
test/subgraph/SubgraphEvents.test.ts
Normal file
458
test/subgraph/SubgraphEvents.test.ts
Normal file
@@ -0,0 +1,458 @@
|
||||
import { describe, expect, vi } from "vitest"
|
||||
|
||||
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
|
||||
|
||||
const input = subgraph.addInput("test_input", "number")
|
||||
|
||||
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.input).toBe(input)
|
||||
})
|
||||
|
||||
subgraphTest("dispatches output-added with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const output = subgraph.addOutput("test_output", "string")
|
||||
|
||||
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.output).toBe(output)
|
||||
})
|
||||
|
||||
subgraphTest("dispatches removing-input with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const input = subgraph.addInput("to_remove", "boolean")
|
||||
|
||||
capture.clear()
|
||||
|
||||
subgraph.removeInput(input)
|
||||
|
||||
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.input).toBe(input)
|
||||
})
|
||||
|
||||
subgraphTest("dispatches removing-output with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const output = subgraph.addOutput("to_remove", "number")
|
||||
|
||||
capture.clear()
|
||||
|
||||
subgraph.removeOutput(output)
|
||||
|
||||
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.output).toBe(output)
|
||||
})
|
||||
|
||||
subgraphTest("dispatches renaming-input with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const input = subgraph.addInput("old_name", "string")
|
||||
|
||||
capture.clear()
|
||||
|
||||
subgraph.renameInput(input, "new_name")
|
||||
|
||||
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.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")
|
||||
})
|
||||
|
||||
subgraphTest("dispatches renaming-output with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const output = subgraph.addOutput("old_name", "number")
|
||||
|
||||
capture.clear()
|
||||
|
||||
subgraph.renameOutput(output, "new_name")
|
||||
|
||||
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.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")
|
||||
})
|
||||
|
||||
subgraphTest("dispatches adding-input with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addInput("test_input", "number")
|
||||
|
||||
const addingEvents = capture.getEventsByType("adding-input")
|
||||
expect(addingEvents).toHaveLength(1)
|
||||
|
||||
expect(addingEvents[0].detail).toEqual({
|
||||
name: "test_input",
|
||||
type: "number",
|
||||
})
|
||||
})
|
||||
|
||||
subgraphTest("dispatches adding-output with correct payload", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addOutput("test_output", "string")
|
||||
|
||||
const addingEvents = capture.getEventsByType("adding-output")
|
||||
expect(addingEvents).toHaveLength(1)
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
// 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 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 }) => {
|
||||
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.addInput("test", "number")
|
||||
|
||||
expect(executionOrder).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
emptySubgraph.events.addEventListener("input-added", handler)
|
||||
|
||||
emptySubgraph.addInput("test", "number")
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphEvents - Event Sequence Testing", () => {
|
||||
subgraphTest("maintains correct event sequence for inputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addInput("input1", "number")
|
||||
|
||||
verifyEventSequence(capture.events, [
|
||||
"adding-input",
|
||||
"input-added",
|
||||
])
|
||||
})
|
||||
|
||||
subgraphTest("maintains correct event sequence for outputs", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
subgraph.addOutput("output1", "string")
|
||||
|
||||
verifyEventSequence(capture.events, [
|
||||
"adding-output",
|
||||
"output-added",
|
||||
])
|
||||
})
|
||||
|
||||
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")
|
||||
|
||||
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 }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
const handler1 = vi.fn(() => {
|
||||
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.addInput("test", "number")
|
||||
|
||||
expect(handler1).toHaveBeenCalled()
|
||||
expect(handler2).toHaveBeenCalled()
|
||||
expect(handler3).toHaveBeenCalled()
|
||||
|
||||
const addedEvents = capture.getEventsByType("input-added")
|
||||
expect(addedEvents).toHaveLength(1)
|
||||
})
|
||||
|
||||
subgraphTest("validates event timestamps are properly ordered", ({ eventCapture }) => {
|
||||
const { subgraph, capture } = eventCapture
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
const input = emptySubgraph.addInput("test", "number")
|
||||
|
||||
emptySubgraph.removeInput(input)
|
||||
|
||||
expect(emptySubgraph.inputs).toContain(input)
|
||||
expect(preventHandler).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
subgraphTest("supports preventDefault() for output removal", ({ emptySubgraph }) => {
|
||||
const preventHandler = vi.fn((event: Event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
emptySubgraph.events.addEventListener("removing-output", preventHandler)
|
||||
|
||||
const output = emptySubgraph.addOutput("test", "number")
|
||||
|
||||
emptySubgraph.removeOutput(output)
|
||||
|
||||
expect(emptySubgraph.outputs).toContain(output)
|
||||
expect(preventHandler).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
subgraphTest("allows removal when not prevented", ({ emptySubgraph }) => {
|
||||
const allowHandler = vi.fn()
|
||||
|
||||
emptySubgraph.events.addEventListener("removing-input", allowHandler)
|
||||
|
||||
const input = emptySubgraph.addInput("test", "number")
|
||||
|
||||
emptySubgraph.removeInput(input)
|
||||
|
||||
expect(emptySubgraph.inputs).not.toContain(input)
|
||||
expect(emptySubgraph.inputs).toHaveLength(0)
|
||||
expect(allowHandler).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
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 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 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 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 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 removingOutputEvent = capture.getEventsByType("removing-output")[0]
|
||||
expect(removingOutputEvent.detail).toEqual({
|
||||
output: expect.any(Object),
|
||||
index: expect.any(Number),
|
||||
})
|
||||
})
|
||||
})
|
||||
351
test/subgraph/SubgraphIO.test.ts
Normal file
351
test/subgraph/SubgraphIO.test.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { LGraphNode } from "@/litegraph"
|
||||
|
||||
import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
import {
|
||||
createTestSubgraph,
|
||||
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
|
||||
|
||||
subgraph.addInput("test_input", "number")
|
||||
|
||||
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)
|
||||
|
||||
externalNode.connect(0, subgraphNode, 0)
|
||||
|
||||
// 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")
|
||||
})
|
||||
})
|
||||
|
||||
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")
|
||||
|
||||
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)
|
||||
|
||||
subgraphNode.connect(0, externalNode, 0)
|
||||
|
||||
// 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")
|
||||
})
|
||||
})
|
||||
|
||||
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 externalTarget = new LGraphNode("External Target")
|
||||
externalTarget.addInput("in", "number")
|
||||
parentGraph.add(externalTarget)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
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 externalTarget = new LGraphNode("External Target")
|
||||
externalTarget.addInput("in", "number")
|
||||
parentGraph.add(externalTarget)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
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 externalTarget = new LGraphNode("External Target")
|
||||
externalTarget.addInput("in", "*")
|
||||
parentGraph.add(externalTarget)
|
||||
|
||||
externalSource.connect(0, subgraphNode, 0)
|
||||
subgraphNode.connect(0, externalTarget, 0)
|
||||
|
||||
const inputBoundaryLink = subgraphNode.inputs[0].link
|
||||
const outputBoundaryLink = externalTarget.inputs[0].link
|
||||
|
||||
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!
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphIO - Advanced Scenarios", () => {
|
||||
it("handles multiple inputs and outputs with complex connections", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Complex IO Test",
|
||||
inputs: [
|
||||
{ name: "input1", type: "number" },
|
||||
{ name: "input2", type: "string" },
|
||||
{ name: "input3", type: "boolean" },
|
||||
],
|
||||
outputs: [
|
||||
{ name: "output1", type: "number" },
|
||||
{ name: "output2", type: "string" },
|
||||
],
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Should have correct number of slots
|
||||
expect(subgraphNode.inputs.length).toBe(3)
|
||||
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")
|
||||
})
|
||||
|
||||
it("handles dynamic slot creation and removal", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
name: "Dynamic IO Test",
|
||||
})
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Start with no slots
|
||||
expect(subgraphNode.inputs.length).toBe(0)
|
||||
expect(subgraphNode.outputs.length).toBe(0)
|
||||
|
||||
// Add slots dynamically
|
||||
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")
|
||||
|
||||
// Remove slots
|
||||
subgraph.removeInput(subgraph.inputs[0])
|
||||
subgraph.removeOutput(subgraph.outputs[0])
|
||||
|
||||
// SubgraphNode should automatically update
|
||||
expect(subgraphNode.inputs.length).toBe(0)
|
||||
expect(subgraphNode.outputs.length).toBe(0)
|
||||
})
|
||||
|
||||
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" }],
|
||||
})
|
||||
|
||||
// Create multiple instances
|
||||
const instance1 = createTestSubgraphNode(subgraph)
|
||||
const instance2 = createTestSubgraphNode(subgraph)
|
||||
const instance3 = createTestSubgraphNode(subgraph)
|
||||
|
||||
// All instances should have same slots
|
||||
expect(instance1.inputs.length).toBe(1)
|
||||
expect(instance2.inputs.length).toBe(1)
|
||||
expect(instance3.inputs.length).toBe(1)
|
||||
|
||||
// Modify the subgraph definition
|
||||
subgraph.addInput("new_input", "string")
|
||||
subgraph.addOutput("new_output", "boolean")
|
||||
|
||||
// All instances should automatically update
|
||||
expect(instance1.inputs.length).toBe(2)
|
||||
expect(instance2.inputs.length).toBe(2)
|
||||
expect(instance3.inputs.length).toBe(2)
|
||||
expect(instance1.outputs.length).toBe(2)
|
||||
expect(instance2.outputs.length).toBe(2)
|
||||
expect(instance3.outputs.length).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,8 @@ import { describe, expect, it, vi } from "vitest"
|
||||
import { LGraph } from "@/litegraph"
|
||||
import { SubgraphNode } from "@/subgraph/SubgraphNode"
|
||||
|
||||
// Note: Avoiding createMemoryLeakTest as it relies on non-deterministic GC behavior
|
||||
import { subgraphTest } from "./fixtures/subgraphFixtures"
|
||||
import {
|
||||
createEventCapture,
|
||||
createTestSubgraph,
|
||||
@@ -110,17 +112,17 @@ describe("SubgraphNode Memory Management", () => {
|
||||
expect(finalCalls).toBe(initialCalls) // Main listeners not re-added
|
||||
})
|
||||
|
||||
it("should demonstrate memory leak with multiple instances", () => {
|
||||
it("should demonstrate memory leak with multiple instances (limited scope)", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// Track total listener count
|
||||
const addEventSpy = vi.spyOn(subgraph.events, "addEventListener")
|
||||
let totalListenersAdded = 0
|
||||
|
||||
// Create and "remove" multiple instances
|
||||
// Create and "remove" multiple instances (limited to 3 for test performance)
|
||||
const instances: SubgraphNode[] = []
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const initialCalls = addEventSpy.mock.calls.length
|
||||
const instance = createTestSubgraphNode(subgraph, { id: i })
|
||||
instances.push(instance)
|
||||
@@ -135,42 +137,49 @@ describe("SubgraphNode Memory Management", () => {
|
||||
// All listeners are still registered even though nodes are "removed"
|
||||
expect(totalListenersAdded).toBeGreaterThan(0)
|
||||
|
||||
// In a real scenario, the subgraph would hold references to all these
|
||||
// "removed" instances through their event listeners, preventing GC
|
||||
console.warn(`Memory leak: ${totalListenersAdded} listeners accumulated from 5 instances`)
|
||||
// Document the leak without excessive accumulation
|
||||
console.warn(`Memory leak demonstrated: ${totalListenersAdded} listeners accumulated from ${instances.length} instances`)
|
||||
|
||||
// IMPORTANT: This test intentionally creates a small memory leak to demonstrate the bug.
|
||||
// In production, this would accumulate over time and cause performance issues.
|
||||
// The leak is limited to 3 instances to minimize test suite impact.
|
||||
})
|
||||
})
|
||||
|
||||
describe("Memory Leak Prevention Tests", () => {
|
||||
it.skipIf(!global.gc)("should allow garbage collection after removal with fix", async () => {
|
||||
// This test demonstrates what SHOULD happen after fixing the memory leak
|
||||
// Currently skipped because global.gc may not be available
|
||||
|
||||
it("verifies proper cleanup workflow exists", () => {
|
||||
// This test documents the expected cleanup workflow without relying on GC
|
||||
const subgraph = createTestSubgraph()
|
||||
let nodeRef: WeakRef<SubgraphNode>
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
{
|
||||
const node = createTestSubgraphNode(subgraph)
|
||||
nodeRef = new WeakRef(node)
|
||||
|
||||
// TODO: Implement proper cleanup (AbortController pattern)
|
||||
// This would need to be implemented in the actual SubgraphNode class
|
||||
node.onRemoved()
|
||||
// Track cleanup state
|
||||
const cleanupState = {
|
||||
inputListenersCleanedUp: false,
|
||||
mainListenersCleanedUp: false,
|
||||
widgetReferencesCleared: false,
|
||||
}
|
||||
|
||||
// Force garbage collection
|
||||
global.gc!()
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
// Check if input listeners exist and are set up
|
||||
const input = subgraphNode.inputs[0]
|
||||
|
||||
// With proper cleanup, this should pass
|
||||
// Currently will likely fail due to event listener references
|
||||
const isCollected = nodeRef.deref() === undefined
|
||||
if (!isCollected) {
|
||||
console.warn("SubgraphNode was not garbage collected - memory leak confirmed")
|
||||
// Call cleanup
|
||||
subgraphNode.onRemoved()
|
||||
|
||||
// Verify what gets cleaned up
|
||||
if (input && "_listenerController" in input && (input as any)._listenerController) {
|
||||
cleanupState.inputListenersCleanedUp = (input as any)._listenerController.signal.aborted === true
|
||||
expect(cleanupState.inputListenersCleanedUp).toBe(true) // This works when input exists
|
||||
} else {
|
||||
// If no input or no listener controller, that's also valid
|
||||
cleanupState.inputListenersCleanedUp = true
|
||||
expect(cleanupState.inputListenersCleanedUp).toBe(true)
|
||||
}
|
||||
|
||||
// This test documents what we want to achieve
|
||||
// expect(nodeRef.deref()).toBeUndefined()
|
||||
// TODO: These should be true after proper implementation
|
||||
// expect(cleanupState.mainListenersCleanedUp).toBe(true)
|
||||
// expect(cleanupState.widgetReferencesCleared).toBe(true)
|
||||
|
||||
// This test serves as documentation for what needs to be implemented
|
||||
})
|
||||
|
||||
it("should clean up widget references properly", () => {
|
||||
@@ -365,11 +374,11 @@ describe("Performance Impact of Memory Leak", () => {
|
||||
it("measures event handler overhead with multiple instances", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
// Create multiple instances (simulating real usage)
|
||||
// Create multiple instances (reduced from 50 to 20 for test efficiency)
|
||||
const instances: SubgraphNode[] = []
|
||||
const startTime = performance.now()
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
instances.push(createTestSubgraphNode(subgraph, { id: i }))
|
||||
}
|
||||
|
||||
@@ -380,7 +389,7 @@ describe("Performance Impact of Memory Leak", () => {
|
||||
subgraph.addInput("performance_test", "number")
|
||||
const eventTime = performance.now() - eventStartTime
|
||||
|
||||
console.log(`Created 50 instances in ${creationTime.toFixed(2)}ms`)
|
||||
console.log(`Created ${instances.length} instances in ${creationTime.toFixed(2)}ms`)
|
||||
console.log(`Event dispatch took ${eventTime.toFixed(2)}ms (${instances.length} listeners)`)
|
||||
|
||||
// More instances = more event listeners = slower event handling
|
||||
@@ -389,8 +398,8 @@ describe("Performance Impact of Memory Leak", () => {
|
||||
})
|
||||
|
||||
it("demonstrates listener accumulation impact", () => {
|
||||
// Test with different numbers of instances
|
||||
const testCases = [10, 25, 50]
|
||||
// Test with different numbers of instances (reduced scale for efficiency)
|
||||
const testCases = [5, 10, 15]
|
||||
|
||||
for (const instanceCount of testCases) {
|
||||
// Clean test - create fresh subgraph
|
||||
@@ -413,3 +422,291 @@ describe("Performance Impact of Memory Leak", () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
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++) {
|
||||
const subgraphNode = createTestSubgraphNode(emptySubgraph)
|
||||
rootGraph.add(subgraphNode)
|
||||
nodes.push(subgraphNode)
|
||||
}
|
||||
|
||||
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", () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphId = subgraph.id
|
||||
|
||||
// Add subgraph to root graph registry
|
||||
rootGraph.subgraphs.set(subgraphId, subgraph)
|
||||
expect(rootGraph.subgraphs.has(subgraphId)).toBe(true)
|
||||
expect(rootGraph.subgraphs.get(subgraphId)).toBe(subgraph)
|
||||
|
||||
// Remove subgraph from registry
|
||||
rootGraph.subgraphs.delete(subgraphId)
|
||||
expect(rootGraph.subgraphs.has(subgraphId)).toBe(false)
|
||||
})
|
||||
|
||||
it("maintains proper parent-child references", () => {
|
||||
const rootGraph = new LGraph()
|
||||
const subgraph = createTestSubgraph({ nodeCount: 2 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Before adding to graph, node might already have a graph reference
|
||||
// (This depends on how createTestSubgraphNode works)
|
||||
|
||||
// Add to graph
|
||||
rootGraph.add(subgraphNode)
|
||||
expect(subgraphNode.graph).toBe(rootGraph)
|
||||
expect(rootGraph.nodes).toContain(subgraphNode)
|
||||
|
||||
// Remove from graph
|
||||
rootGraph.remove(subgraphNode)
|
||||
expect(rootGraph.nodes).not.toContain(subgraphNode)
|
||||
|
||||
// After removal, graph reference behavior may vary by implementation
|
||||
// The important thing is that it's removed from the graph's nodes array
|
||||
})
|
||||
|
||||
it("prevents circular reference creation", () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 1 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Subgraph should not contain its own instance node
|
||||
expect(subgraph.nodes).not.toContain(subgraphNode)
|
||||
|
||||
// If circular references were attempted, they should be detected
|
||||
// (This documents the expected behavior - implementation may vary)
|
||||
expect(subgraphNode.subgraph).toBe(subgraph)
|
||||
expect(subgraph.nodes.includes(subgraphNode)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
// 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 }) => {
|
||||
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" }
|
||||
|
||||
if (subgraphNode.widgets) {
|
||||
subgraphNode.widgets.push(widget1, widget2)
|
||||
expect(subgraphNode.widgets.length).toBe(initialWidgetCount + 2)
|
||||
}
|
||||
|
||||
// Remove widgets
|
||||
if (subgraphNode.widgets) {
|
||||
subgraphNode.widgets.length = initialWidgetCount
|
||||
expect(subgraphNode.widgets.length).toBe(initialWidgetCount)
|
||||
}
|
||||
})
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
// Note: Other references may still exist - this documents current behavior
|
||||
// In a proper implementation, onRemoved should clean these up too
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
parentGraph.remove(nodeA)
|
||||
parentGraph.remove(nodeB)
|
||||
|
||||
expect(parentGraph.nodes.length).toBe(1) // Only the original subgraphNode remains
|
||||
})
|
||||
|
||||
it("handles many instances without issues", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "stress_input", type: "number" }],
|
||||
outputs: [{ name: "stress_output", type: "number" }],
|
||||
})
|
||||
|
||||
const rootGraph = new LGraph()
|
||||
const instances = []
|
||||
|
||||
// Create instances (reduced from 50 to 25 for test efficiency)
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const instance = createTestSubgraphNode(subgraph)
|
||||
rootGraph.add(instance)
|
||||
instances.push(instance)
|
||||
}
|
||||
|
||||
expect(instances.length).toBe(25)
|
||||
expect(rootGraph.nodes.length).toBe(25)
|
||||
|
||||
// Remove all instances (proper cleanup)
|
||||
for (const instance of instances) {
|
||||
rootGraph.remove(instance)
|
||||
}
|
||||
|
||||
expect(rootGraph.nodes.length).toBe(0)
|
||||
})
|
||||
|
||||
it("maintains consistent behavior across multiple cycles", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const rootGraph = new LGraph()
|
||||
|
||||
for (let cycle = 0; cycle < 10; cycle++) {
|
||||
const instances = []
|
||||
|
||||
// Create instances
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const instance = createTestSubgraphNode(subgraph)
|
||||
rootGraph.add(instance)
|
||||
instances.push(instance)
|
||||
}
|
||||
|
||||
expect(rootGraph.nodes.length).toBe(10)
|
||||
|
||||
// Remove instances
|
||||
for (const instance of instances) {
|
||||
rootGraph.remove(instance)
|
||||
}
|
||||
|
||||
expect(rootGraph.nodes.length).toBe(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,9 +64,7 @@ describe("SubgraphNode Construction", () => {
|
||||
expect(subgraphNode.inputs).toHaveLength(subgraph.inputs.length)
|
||||
expect(subgraphNode.outputs).toHaveLength(subgraph.outputs.length)
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphNode Synchronization", () => {
|
||||
subgraphTest("should update slots when subgraph definition changes", ({ subgraphWithNode }) => {
|
||||
const { subgraph, subgraphNode } = subgraphWithNode
|
||||
|
||||
@@ -80,7 +78,9 @@ describe("SubgraphNode Synchronization", () => {
|
||||
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", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -358,7 +358,20 @@ describe("SubgraphNode Execution", () => {
|
||||
// This causes infinite recursion instead of throwing the error
|
||||
// Fix: Change `new Set(visited)` to just `visited`
|
||||
})
|
||||
|
||||
it.todo("should handle nested subgraph execution")
|
||||
|
||||
it.todo("should resolve cross-boundary links")
|
||||
})
|
||||
|
||||
describe("SubgraphNode Edge Cases", () => {
|
||||
it.todo("should detect circular references")
|
||||
|
||||
it.todo("should handle deep nesting")
|
||||
|
||||
it.todo("should validate against MAX_NESTED_SUBGRAPHS")
|
||||
})
|
||||
|
||||
describe("SubgraphNode Integration", () => {
|
||||
it("should be addable to a parent graph", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# Subgraph Testing Fixtures and Utilities
|
||||
|
||||
Testing infrastructure for LiteGraph's subgraph functionality. A subgraph is a graph-within-a-graph that can be reused as a single node, with input/output slots mapping to internal IO nodes.
|
||||
This directory contains the testing infrastructure for LiteGraph's subgraph functionality. These utilities provide a consistent, easy-to-use API for writing subgraph tests.
|
||||
|
||||
## What is a Subgraph?
|
||||
|
||||
A subgraph in LiteGraph is a graph-within-a-graph that can be reused as a single node. It has:
|
||||
- Input slots that map to an internal input node
|
||||
- Output slots that map to an internal output node
|
||||
- Internal nodes and connections
|
||||
- The ability to be instantiated multiple times as SubgraphNode instances
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -22,9 +30,10 @@ it("should do something", () => {
|
||||
})
|
||||
|
||||
// Option 2: Use pre-configured fixtures
|
||||
subgraphTest("should handle events", ({ simpleSubgraph }) => {
|
||||
subgraphTest("should handle events", ({ simpleSubgraph, eventCapture }) => {
|
||||
// simpleSubgraph comes pre-configured with 1 input, 1 output, and 2 nodes
|
||||
expect(simpleSubgraph.inputs).toHaveLength(1)
|
||||
// Your test logic here
|
||||
})
|
||||
```
|
||||
|
||||
@@ -230,10 +239,73 @@ interface NestedSubgraphOptions {
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. **Array items don't have index property** - Use `indexOf()` instead
|
||||
2. **IO nodes have `subgraph` property** - Not `graph` like regular nodes
|
||||
3. **Links are stored in a Map** - Use `.size` not `.length`
|
||||
4. **Event detail structures** - Check exact property names:
|
||||
- `"adding-input"`: `{ name, type }`
|
||||
- `"input-added"`: `{ input, index }`
|
||||
1. **Array vs Index**: The `inputs` and `outputs` arrays don't have an `index` property on items. Use `indexOf()`:
|
||||
```typescript
|
||||
// ❌ Wrong
|
||||
expect(input.index).toBe(0)
|
||||
|
||||
// ✅ Correct
|
||||
expect(subgraph.inputs.indexOf(input)).toBe(0)
|
||||
```
|
||||
|
||||
2. **Graph vs Subgraph Property**: SubgraphInputNode/OutputNode have `subgraph`, not `graph`:
|
||||
```typescript
|
||||
// ❌ Wrong
|
||||
expect(inputNode.graph).toBe(subgraph)
|
||||
|
||||
// ✅ Correct
|
||||
expect(inputNode.subgraph).toBe(subgraph)
|
||||
```
|
||||
|
||||
3. **Event Detail Structure**: Events have specific detail structures:
|
||||
```typescript
|
||||
// Input events
|
||||
"adding-input": { name: string, type: string }
|
||||
"input-added": { input: SubgraphInput, index: number }
|
||||
|
||||
// Output events
|
||||
"adding-output": { name: string, type: string }
|
||||
"output-added": { output: SubgraphOutput, index: number }
|
||||
```
|
||||
|
||||
4. **Links are stored in a Map**: Use `.size` not `.length`:
|
||||
```typescript
|
||||
// ❌ Wrong
|
||||
expect(subgraph.links.length).toBe(1)
|
||||
|
||||
// ✅ Correct
|
||||
expect(subgraph.links.size).toBe(1)
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
- Always use helper functions instead of manual setup
|
||||
- Use fixtures for common scenarios to avoid repetitive code
|
||||
- Clean up event listeners with `capture.cleanup()` after event tests
|
||||
- Use `verifyEventSequence()` to test event ordering
|
||||
- Remember fixtures are created fresh for each test (no shared state)
|
||||
- Use `assertSubgraphStructure()` for comprehensive validation
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
- Use `logSubgraphStructure(subgraph)` to print subgraph details
|
||||
- Check `subgraph.rootGraph` to verify graph hierarchy
|
||||
- Event capture includes timestamps for debugging timing issues
|
||||
- All factory functions accept optional parameters for customization
|
||||
|
||||
## Adding New Test Utilities
|
||||
|
||||
When extending the test infrastructure:
|
||||
|
||||
1. Add new helper functions to `subgraphHelpers.ts`
|
||||
2. Add new fixtures to `subgraphFixtures.ts`
|
||||
3. Update this README with usage examples
|
||||
4. Follow existing patterns for consistency
|
||||
5. Add TypeScript types for all parameters
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- Helper functions are optimized for test clarity, not performance
|
||||
- Use `structuredClone()` for deep copying test data
|
||||
- Event capture systems automatically clean up listeners
|
||||
- Fixtures are created fresh for each test to avoid state contamination
|
||||
|
||||
244
test/subgraph/fixtures/advancedEventHelpers.ts
Normal file
244
test/subgraph/fixtures/advancedEventHelpers.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import type { CapturedEvent } from "./subgraphHelpers"
|
||||
|
||||
import { expect } from "vitest"
|
||||
|
||||
/**
|
||||
* Extended captured event with additional metadata not in the base infrastructure
|
||||
*/
|
||||
export interface ExtendedCapturedEvent<T = unknown> extends CapturedEvent<T> {
|
||||
defaultPrevented: boolean
|
||||
bubbles: boolean
|
||||
cancelable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an enhanced event capture that includes additional event properties
|
||||
* This extends the basic createEventCapture with more metadata
|
||||
*/
|
||||
export function createExtendedEventCapture<T = unknown>(
|
||||
eventTarget: EventTarget,
|
||||
eventTypes: string[],
|
||||
) {
|
||||
const capturedEvents: ExtendedCapturedEvent<T>[] = []
|
||||
const listeners: Array<() => void> = []
|
||||
|
||||
for (const eventType of eventTypes) {
|
||||
const listener = (event: Event) => {
|
||||
capturedEvents.push({
|
||||
type: eventType,
|
||||
detail: (event as CustomEvent<T>).detail,
|
||||
timestamp: Date.now(),
|
||||
defaultPrevented: event.defaultPrevented,
|
||||
bubbles: event.bubbles,
|
||||
cancelable: event.cancelable,
|
||||
})
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(eventType, listener)
|
||||
listeners.push(() => eventTarget.removeEventListener(eventType, listener))
|
||||
}
|
||||
|
||||
return {
|
||||
events: capturedEvents,
|
||||
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)
|
||||
if (existingEvent) return existingEvent
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
eventTarget.removeEventListener(type, eventListener)
|
||||
reject(new Error(`Event ${type} not received within ${timeoutMs}ms`))
|
||||
}, timeoutMs)
|
||||
|
||||
const eventListener = (_event: Event) => {
|
||||
const capturedEvent = capturedEvents.find(e => e.type === type)
|
||||
if (capturedEvent) {
|
||||
clearTimeout(timeout)
|
||||
eventTarget.removeEventListener(type, eventListener)
|
||||
resolve(capturedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(type, eventListener)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Wait for a sequence of events to occur in order
|
||||
*/
|
||||
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)) {
|
||||
return capturedEvents.slice(0, expectedSequence.length)
|
||||
}
|
||||
}
|
||||
|
||||
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}`))
|
||||
}, 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)) {
|
||||
cleanup()
|
||||
resolve(capturedEvents.slice(0, expectedSequence.length))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eventListener = () => checkSequence()
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout)
|
||||
for (const type of expectedSequence) {
|
||||
eventTarget.removeEventListener(type, eventListener)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for all expected event types
|
||||
for (const type of expectedSequence) {
|
||||
eventTarget.addEventListener(type, eventListener)
|
||||
}
|
||||
|
||||
// Initial check in case events already exist
|
||||
checkSequence()
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for memory leak testing
|
||||
*/
|
||||
export interface MemoryLeakTestOptions {
|
||||
cycles?: number
|
||||
instancesPerCycle?: number
|
||||
gcAfterEach?: boolean
|
||||
maxMemoryGrowth?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a memory leak test factory
|
||||
* Useful for testing that event listeners and references are properly cleaned up
|
||||
*/
|
||||
export function createMemoryLeakTest<T>(
|
||||
setupFn: () => { ref: WeakRef<T>, cleanup: () => void },
|
||||
options: MemoryLeakTestOptions = {},
|
||||
) {
|
||||
const {
|
||||
cycles = 1,
|
||||
instancesPerCycle = 1,
|
||||
gcAfterEach = true,
|
||||
maxMemoryGrowth = 0,
|
||||
} = options
|
||||
|
||||
return async () => {
|
||||
const refs: WeakRef<T>[] = []
|
||||
const initialMemory = process.memoryUsage?.()?.heapUsed || 0
|
||||
|
||||
for (let cycle = 0; cycle < cycles; cycle++) {
|
||||
const cycleRefs: WeakRef<T>[] = []
|
||||
|
||||
for (let instance = 0; instance < instancesPerCycle; instance++) {
|
||||
const { ref, cleanup } = setupFn()
|
||||
cycleRefs.push(ref)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
refs.push(...cycleRefs)
|
||||
|
||||
if (gcAfterEach && global.gc) {
|
||||
global.gc()
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
}
|
||||
}
|
||||
|
||||
// Final garbage collection
|
||||
if (global.gc) {
|
||||
global.gc()
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
|
||||
// Check if objects were collected
|
||||
const uncollectedRefs = refs.filter(ref => ref.deref() !== undefined)
|
||||
if (uncollectedRefs.length > 0) {
|
||||
console.warn(`${uncollectedRefs.length} objects were not garbage collected`)
|
||||
}
|
||||
}
|
||||
|
||||
// Memory growth check
|
||||
if (maxMemoryGrowth > 0 && process.memoryUsage) {
|
||||
const finalMemory = process.memoryUsage().heapUsed
|
||||
const memoryGrowth = finalMemory - initialMemory
|
||||
|
||||
if (memoryGrowth > maxMemoryGrowth) {
|
||||
throw new Error(`Memory growth ${memoryGrowth} bytes exceeds limit ${maxMemoryGrowth} bytes`)
|
||||
}
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a performance monitor for event operations
|
||||
*/
|
||||
export function createEventPerformanceMonitor() {
|
||||
const measurements: Array<{
|
||||
operation: string
|
||||
duration: number
|
||||
timestamp: number
|
||||
}> = []
|
||||
|
||||
return {
|
||||
measure: <T>(operation: string, fn: () => T): T => {
|
||||
const start = performance.now()
|
||||
const result = fn()
|
||||
const end = performance.now()
|
||||
|
||||
measurements.push({
|
||||
operation,
|
||||
duration: end - start,
|
||||
timestamp: start,
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
getMeasurements: () => [...measurements],
|
||||
|
||||
getAverageDuration: (operation: string) => {
|
||||
const operationMeasurements = measurements.filter(m => m.operation === operation)
|
||||
if (operationMeasurements.length === 0) return 0
|
||||
|
||||
const totalDuration = operationMeasurements.reduce((sum, m) => sum + m.duration, 0)
|
||||
return totalDuration / operationMeasurements.length
|
||||
},
|
||||
|
||||
clear: () => { measurements.length = 0 },
|
||||
|
||||
assertPerformance: (operation: string, maxDuration: number) => {
|
||||
const measurements = this.getMeasurements()
|
||||
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
|
||||
expect(avgDuration).toBeLessThan(maxDuration)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Vitest Fixtures for Subgraph Testing
|
||||
*
|
||||
* Reusable Vitest fixtures for subgraph testing.
|
||||
* Each fixture provides a clean, pre-configured subgraph
|
||||
* This file provides reusable Vitest fixtures that other developers can use
|
||||
* in their test files. Each fixture provides a clean, pre-configured subgraph
|
||||
* setup for different testing scenarios.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Test Helper Functions for Subgraph Testing
|
||||
*
|
||||
* Core utilities for creating and testing subgraphs.
|
||||
* Provides consistent APIs for test subgraph creation, node management,
|
||||
* and behavior verification.
|
||||
* This file contains the core utilities that all subgraph developers will use.
|
||||
* These functions provide consistent ways to create test subgraphs, nodes, and
|
||||
* verify their behavior.
|
||||
*/
|
||||
|
||||
import type { ISlotType, NodeId } from "@/litegraph"
|
||||
@@ -55,16 +55,20 @@ export interface CapturedEvent<T = unknown> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a test subgraph with the specified configuration.
|
||||
* This is the primary function for creating test subgraphs.
|
||||
* Creates a test subgraph with specified inputs, outputs, and nodes.
|
||||
* This is the primary function for creating subgraphs in tests.
|
||||
* @param options Configuration options for the subgraph
|
||||
* @returns A configured Subgraph instance
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Create empty subgraph
|
||||
* const subgraph = createTestSubgraph()
|
||||
*
|
||||
* // Create subgraph with specific I/O
|
||||
* const subgraph = createTestSubgraph({
|
||||
* name: "My Test Subgraph",
|
||||
* inputCount: 2,
|
||||
* outputCount: 1
|
||||
* inputs: [{ name: "value", type: "number" }],
|
||||
* outputs: [{ name: "result", type: "string" }],
|
||||
* nodeCount: 3
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
@@ -78,7 +82,6 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph
|
||||
if (options.outputs && options.outputCount) {
|
||||
throw new Error(`Cannot specify both 'outputs' array and 'outputCount'. Choose one approach. Received options: ${JSON.stringify(options)}`)
|
||||
}
|
||||
|
||||
const rootGraph = new LGraph()
|
||||
|
||||
// Create the base subgraph data
|
||||
@@ -153,14 +156,17 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph
|
||||
|
||||
/**
|
||||
* Creates a SubgraphNode instance from a subgraph definition.
|
||||
* @param subgraph The subgraph definition to instantiate
|
||||
* @param options Configuration options for the node instance
|
||||
* @returns A SubgraphNode instance
|
||||
* The node is automatically added to a test parent graph.
|
||||
* @param subgraph The subgraph definition to create a node from
|
||||
* @param options Configuration options for the subgraph node
|
||||
* @returns A configured SubgraphNode instance
|
||||
* @example
|
||||
* ```typescript
|
||||
* const subgraph = createTestSubgraph()
|
||||
* const subgraph = createTestSubgraph({ inputs: [{ name: "value", type: "number" }] })
|
||||
* const subgraphNode = createTestSubgraphNode(subgraph, {
|
||||
* pos: [100, 200]
|
||||
* id: 42,
|
||||
* pos: [100, 200],
|
||||
* size: [180, 100]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user