|
5 | 5 | * IO synchronization, and edge cases. |
6 | 6 | */ |
7 | 7 |
|
8 | | -import { describe, expect, it } from "vitest" |
| 8 | +import { describe, expect, it, vi } from "vitest" |
9 | 9 |
|
10 | 10 | import { LGraph, Subgraph } from "@/litegraph" |
11 | 11 |
|
@@ -492,3 +492,87 @@ describe("Foundation Test Utilities", () => { |
492 | 492 | expect(parentGraph.nodes).toContain(subgraphNode) |
493 | 493 | }) |
494 | 494 | }) |
| 495 | + |
| 496 | +describe("SubgraphNode Cleanup", () => { |
| 497 | + it("should clean up event listeners when removed", () => { |
| 498 | + const rootGraph = new LGraph() |
| 499 | + const subgraph = createTestSubgraph() |
| 500 | + |
| 501 | + // Create and add two nodes |
| 502 | + const node1 = createTestSubgraphNode(subgraph) |
| 503 | + const node2 = createTestSubgraphNode(subgraph) |
| 504 | + rootGraph.add(node1) |
| 505 | + rootGraph.add(node2) |
| 506 | + |
| 507 | + // Verify both nodes start with no inputs |
| 508 | + expect(node1.inputs.length).toBe(0) |
| 509 | + expect(node2.inputs.length).toBe(0) |
| 510 | + |
| 511 | + // Remove node2 |
| 512 | + rootGraph.remove(node2) |
| 513 | + |
| 514 | + // Now trigger an event - only node1 should respond |
| 515 | + subgraph.events.dispatch("input-added", { |
| 516 | + input: { name: "test", type: "number", id: "test-id" } as any, |
| 517 | + }) |
| 518 | + |
| 519 | + // Only node1 should have added an input |
| 520 | + expect(node1.inputs.length).toBe(1) // node1 responds |
| 521 | + expect(node2.inputs.length).toBe(0) // node2 should NOT respond (but currently does) |
| 522 | + }) |
| 523 | + |
| 524 | + it("should not accumulate handlers over multiple add/remove cycles", () => { |
| 525 | + const rootGraph = new LGraph() |
| 526 | + const subgraph = createTestSubgraph() |
| 527 | + |
| 528 | + // Add and remove nodes multiple times |
| 529 | + const removedNodes: SubgraphNode[] = [] |
| 530 | + for (let i = 0; i < 3; i++) { |
| 531 | + const node = createTestSubgraphNode(subgraph) |
| 532 | + rootGraph.add(node) |
| 533 | + rootGraph.remove(node) |
| 534 | + removedNodes.push(node) |
| 535 | + } |
| 536 | + |
| 537 | + // All nodes should have 0 inputs |
| 538 | + for (const node of removedNodes) { |
| 539 | + expect(node.inputs.length).toBe(0) |
| 540 | + } |
| 541 | + |
| 542 | + // Trigger an event - no nodes should respond |
| 543 | + subgraph.events.dispatch("input-added", { |
| 544 | + input: { name: "test", type: "number", id: "test-id" } as any, |
| 545 | + }) |
| 546 | + |
| 547 | + // Without cleanup: all 3 removed nodes would have added an input |
| 548 | + // With cleanup: no nodes should have added an input |
| 549 | + for (const node of removedNodes) { |
| 550 | + expect(node.inputs.length).toBe(0) // Should stay 0 after cleanup |
| 551 | + } |
| 552 | + }) |
| 553 | + |
| 554 | + it("should clean up input listener controllers on removal", () => { |
| 555 | + const rootGraph = new LGraph() |
| 556 | + const subgraph = createTestSubgraph({ |
| 557 | + inputs: [{ name: "in1", type: "number" }, { name: "in2", type: "string" }], |
| 558 | + }) |
| 559 | + |
| 560 | + const subgraphNode = createTestSubgraphNode(subgraph) |
| 561 | + rootGraph.add(subgraphNode) |
| 562 | + |
| 563 | + // Verify listener controllers exist |
| 564 | + expect(subgraphNode.inputs[0]._listenerController).toBeDefined() |
| 565 | + expect(subgraphNode.inputs[1]._listenerController).toBeDefined() |
| 566 | + |
| 567 | + // Track abort calls |
| 568 | + const abortSpy1 = vi.spyOn(subgraphNode.inputs[0]._listenerController!, "abort") |
| 569 | + const abortSpy2 = vi.spyOn(subgraphNode.inputs[1]._listenerController!, "abort") |
| 570 | + |
| 571 | + // Remove node |
| 572 | + rootGraph.remove(subgraphNode) |
| 573 | + |
| 574 | + // Verify abort was called on each controller |
| 575 | + expect(abortSpy1).toHaveBeenCalledTimes(1) |
| 576 | + expect(abortSpy2).toHaveBeenCalledTimes(1) |
| 577 | + }) |
| 578 | +}) |
0 commit comments