mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-27 10:14:06 +00:00
Add slot compatibility checking for subgraph slots (#1182)
This commit is contained in:
280
test/subgraph/SubgraphSlotConnections.test.ts
Normal file
280
test/subgraph/SubgraphSlotConnections.test.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { LGraphNode } from "@/litegraph"
|
||||
import { NodeInputSlot } from "@/node/NodeInputSlot"
|
||||
import { NodeOutputSlot } from "@/node/NodeOutputSlot"
|
||||
import { isSubgraphInput, isSubgraphOutput } from "@/subgraph/subgraphUtils"
|
||||
|
||||
import { createTestSubgraph, createTestSubgraphNode } from "./fixtures/subgraphHelpers"
|
||||
|
||||
describe("Subgraph slot connections", () => {
|
||||
describe("SubgraphInput connections", () => {
|
||||
it("should connect to compatible regular input slots", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
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")
|
||||
subgraph.add(node)
|
||||
|
||||
const compatibleSlot = node.inputs[0] as NodeInputSlot
|
||||
const incompatibleSlot = node.inputs[1] as NodeInputSlot
|
||||
|
||||
expect(compatibleSlot.isValidTarget(subgraphInput)).toBe(true)
|
||||
expect(incompatibleSlot.isValidTarget(subgraphInput)).toBe(false)
|
||||
})
|
||||
|
||||
// "not implemented" yet, but the test passes in terms of type checking
|
||||
// it("should connect to compatible SubgraphOutput", () => {
|
||||
// const subgraph = createTestSubgraph({
|
||||
// inputs: [{ name: "test_input", type: "number" }],
|
||||
// outputs: [{ name: "test_output", type: "number" }],
|
||||
// })
|
||||
|
||||
// const subgraphInput = subgraph.inputs[0]
|
||||
// const subgraphOutput = subgraph.outputs[0]
|
||||
|
||||
// expect(subgraphOutput.isValidTarget(subgraphInput)).toBe(true)
|
||||
// })
|
||||
|
||||
it("should not connect to another SubgraphInput", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: "input1", type: "number" },
|
||||
{ name: "input2", type: "number" },
|
||||
],
|
||||
})
|
||||
|
||||
const subgraphInput1 = subgraph.inputs[0]
|
||||
const subgraphInput2 = subgraph.inputs[1]
|
||||
|
||||
expect(subgraphInput2.isValidTarget(subgraphInput1)).toBe(false)
|
||||
})
|
||||
|
||||
it("should not connect to output slots", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "test_input", type: "number" }],
|
||||
})
|
||||
|
||||
const subgraphInput = subgraph.inputs[0]
|
||||
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addOutput("test_output", "number")
|
||||
subgraph.add(node)
|
||||
const outputSlot = node.outputs[0] as NodeOutputSlot
|
||||
|
||||
expect(outputSlot.isValidTarget(subgraphInput)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("SubgraphOutput connections", () => {
|
||||
it("should connect from compatible regular output slots", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addOutput("out", "number")
|
||||
subgraph.add(node)
|
||||
|
||||
const subgraphOutput = subgraph.addOutput("result", "number")
|
||||
const nodeOutput = node.outputs[0]
|
||||
|
||||
expect(subgraphOutput.isValidTarget(nodeOutput)).toBe(true)
|
||||
})
|
||||
|
||||
it("should connect from SubgraphInput", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
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", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
|
||||
const subgraphOutput1 = subgraph.addOutput("result1", "number")
|
||||
const subgraphOutput2 = subgraph.addOutput("result2", "number")
|
||||
|
||||
expect(subgraphOutput1.isValidTarget(subgraphOutput2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Type compatibility", () => {
|
||||
it("should respect type compatibility for SubgraphInput connections", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
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")
|
||||
subgraph.add(node)
|
||||
|
||||
const numberSlot = node.inputs[0] as NodeInputSlot
|
||||
const stringSlot = node.inputs[1] as NodeInputSlot
|
||||
const anySlot = node.inputs[2] as NodeInputSlot
|
||||
const booleanSlot = node.inputs[3] as NodeInputSlot
|
||||
|
||||
expect(numberSlot.isValidTarget(subgraphInput)).toBe(true)
|
||||
expect(stringSlot.isValidTarget(subgraphInput)).toBe(false)
|
||||
expect(anySlot.isValidTarget(subgraphInput)).toBe(true)
|
||||
expect(booleanSlot.isValidTarget(subgraphInput)).toBe(false)
|
||||
})
|
||||
|
||||
it("should respect type compatibility for SubgraphOutput connections", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addOutput("out", "string")
|
||||
subgraph.add(node)
|
||||
|
||||
const subgraphOutput = subgraph.addOutput("result", "number")
|
||||
const nodeOutput = node.outputs[0]
|
||||
|
||||
expect(subgraphOutput.isValidTarget(nodeOutput)).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle wildcard SubgraphInput", () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: "any_input", type: "*" }],
|
||||
})
|
||||
|
||||
const subgraphInput = subgraph.inputs[0]
|
||||
|
||||
const node = new LGraphNode("TestNode")
|
||||
node.addInput("number_slot", "number")
|
||||
subgraph.add(node)
|
||||
|
||||
const numberSlot = node.inputs[0] as NodeInputSlot
|
||||
|
||||
expect(numberSlot.isValidTarget(subgraphInput)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
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")
|
||||
|
||||
expect(isSubgraphInput(subgraphInput)).toBe(true)
|
||||
expect(isSubgraphInput(node.inputs[0])).toBe(false)
|
||||
expect(isSubgraphInput(null)).toBe(false)
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
expect(isSubgraphInput(undefined)).toBe(false)
|
||||
expect(isSubgraphInput({})).toBe(false)
|
||||
})
|
||||
|
||||
it("should correctly identify SubgraphOutput", () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
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)
|
||||
expect(isSubgraphOutput(null)).toBe(false)
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
expect(isSubgraphOutput(undefined)).toBe(false)
|
||||
expect(isSubgraphOutput({})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
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" }],
|
||||
})
|
||||
|
||||
const nestedSubgraph = createTestSubgraph({
|
||||
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")
|
||||
nestedSubgraph.add(regularNode)
|
||||
|
||||
const nestedSubgraphInput = nestedSubgraph.inputs[0]
|
||||
const regularNodeSlot = regularNode.inputs[0] as NodeInputSlot
|
||||
|
||||
expect(regularNodeSlot.isValidTarget(nestedSubgraphInput)).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle multiple levels of nesting", () => {
|
||||
const level1 = createTestSubgraph({
|
||||
inputs: [{ name: "level1_input", type: "string" }],
|
||||
})
|
||||
|
||||
const level2 = createTestSubgraph({
|
||||
inputs: [{ name: "level2_input", type: "string" }],
|
||||
})
|
||||
|
||||
const level3 = createTestSubgraph({
|
||||
inputs: [{ name: "level3_input", type: "string" }],
|
||||
outputs: [{ name: "level3_output", type: "string" }],
|
||||
})
|
||||
|
||||
const level2Node = createTestSubgraphNode(level2)
|
||||
level1.add(level2Node)
|
||||
|
||||
const level3Node = createTestSubgraphNode(level3)
|
||||
level2.add(level3Node)
|
||||
|
||||
const deepNode = new LGraphNode("DeepNode")
|
||||
deepNode.addInput("deep_input", "string")
|
||||
level3.add(deepNode)
|
||||
|
||||
const level3Input = level3.inputs[0]
|
||||
const deepNodeSlot = deepNode.inputs[0] as NodeInputSlot
|
||||
|
||||
expect(deepNodeSlot.isValidTarget(level3Input)).toBe(true)
|
||||
|
||||
const level3Output = level3.outputs[0]
|
||||
expect(level3Output.isValidTarget(level3Input)).toBe(true)
|
||||
})
|
||||
|
||||
it("should maintain type checking across nesting levels", () => {
|
||||
const outer = createTestSubgraph({
|
||||
inputs: [{ name: "outer_number", type: "number" }],
|
||||
})
|
||||
|
||||
const inner = createTestSubgraph({
|
||||
inputs: [
|
||||
{ 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")
|
||||
inner.add(node)
|
||||
|
||||
const innerNumberInput = inner.inputs[0]
|
||||
const innerStringInput = inner.inputs[1]
|
||||
const numberSlot = node.inputs[0] as NodeInputSlot
|
||||
const stringSlot = node.inputs[1] as NodeInputSlot
|
||||
|
||||
expect(numberSlot.isValidTarget(innerNumberInput)).toBe(true)
|
||||
expect(numberSlot.isValidTarget(innerStringInput)).toBe(false)
|
||||
expect(stringSlot.isValidTarget(innerNumberInput)).toBe(false)
|
||||
expect(stringSlot.isValidTarget(innerStringInput)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
181
test/subgraph/SubgraphSlotVisualFeedback.test.ts
Normal file
181
test/subgraph/SubgraphSlotVisualFeedback.test.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { LGraphNode } from "@/litegraph"
|
||||
|
||||
import { createTestSubgraph } from "./fixtures/subgraphHelpers"
|
||||
|
||||
describe("SubgraphSlot visual feedback", () => {
|
||||
let mockCtx: CanvasRenderingContext2D
|
||||
let mockColorContext: any
|
||||
let globalAlphaValues: number[]
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear the array before each test
|
||||
globalAlphaValues = []
|
||||
|
||||
// Create a mock canvas context that tracks all globalAlpha values
|
||||
const mockContext = {
|
||||
_globalAlpha: 1,
|
||||
get globalAlpha() {
|
||||
return this._globalAlpha
|
||||
},
|
||||
set globalAlpha(value: number) {
|
||||
this._globalAlpha = value
|
||||
globalAlphaValues.push(value)
|
||||
},
|
||||
fillStyle: "",
|
||||
strokeStyle: "",
|
||||
lineWidth: 1,
|
||||
beginPath: vi.fn(),
|
||||
arc: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
rect: 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"),
|
||||
}
|
||||
})
|
||||
|
||||
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")
|
||||
subgraph.add(node)
|
||||
|
||||
// Add a subgraph input
|
||||
const subgraphInput = subgraph.addInput("value", "number")
|
||||
|
||||
// Simulate dragging from the subgraph input (which acts as output inside subgraph)
|
||||
const nodeInput = node.inputs[0]
|
||||
|
||||
// Draw the slot with a compatible fromSlot
|
||||
subgraphInput.draw({
|
||||
ctx: mockCtx,
|
||||
colorContext: mockColorContext,
|
||||
fromSlot: nodeInput,
|
||||
editorAlpha: 1,
|
||||
})
|
||||
|
||||
// Should render with full opacity (not 0.4)
|
||||
// Check that 0.4 was NOT set during drawing
|
||||
expect(globalAlphaValues).not.toContain(0.4)
|
||||
})
|
||||
|
||||
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")
|
||||
|
||||
// Draw subgraphInput2 while dragging from subgraphInput1 (incompatible - both are outputs inside subgraph)
|
||||
subgraphInput2.draw({
|
||||
ctx: mockCtx,
|
||||
colorContext: mockColorContext,
|
||||
fromSlot: subgraphInput1,
|
||||
editorAlpha: 1,
|
||||
})
|
||||
|
||||
// Should render with 40% opacity
|
||||
// Check that 0.4 was set during drawing
|
||||
expect(globalAlphaValues).toContain(0.4)
|
||||
})
|
||||
|
||||
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")
|
||||
subgraph.add(node)
|
||||
|
||||
// Add a subgraph output
|
||||
const subgraphOutput = subgraph.addOutput("result", "number")
|
||||
|
||||
// Simulate dragging from a node output
|
||||
const nodeOutput = node.outputs[0]
|
||||
|
||||
// Draw the slot with a compatible fromSlot
|
||||
subgraphOutput.draw({
|
||||
ctx: mockCtx,
|
||||
colorContext: mockColorContext,
|
||||
fromSlot: nodeOutput,
|
||||
editorAlpha: 1,
|
||||
})
|
||||
|
||||
// Should render with full opacity (not 0.4)
|
||||
// Check that 0.4 was NOT set during drawing
|
||||
expect(globalAlphaValues).not.toContain(0.4)
|
||||
})
|
||||
|
||||
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")
|
||||
|
||||
// Draw subgraphOutput2 while dragging from subgraphOutput1 (incompatible - both are inputs inside subgraph)
|
||||
subgraphOutput2.draw({
|
||||
ctx: mockCtx,
|
||||
colorContext: mockColorContext,
|
||||
fromSlot: subgraphOutput1,
|
||||
editorAlpha: 1,
|
||||
})
|
||||
|
||||
// Should render with 40% opacity
|
||||
// Check that 0.4 was set during drawing
|
||||
expect(globalAlphaValues).toContain(0.4)
|
||||
})
|
||||
|
||||
// "not implmeneted yet"
|
||||
// it("should render slots with full opacity when dragging between compatible SubgraphInput and SubgraphOutput", () => {
|
||||
// const subgraph = createTestSubgraph()
|
||||
|
||||
// // Add subgraph input and output with matching types
|
||||
// const subgraphInput = subgraph.addInput("value", "number")
|
||||
// const subgraphOutput = subgraph.addOutput("result", "number")
|
||||
|
||||
// // Draw SubgraphOutput slot while dragging from SubgraphInput
|
||||
// subgraphOutput.draw({
|
||||
// ctx: mockCtx,
|
||||
// colorContext: mockColorContext,
|
||||
// fromSlot: subgraphInput,
|
||||
// editorAlpha: 1,
|
||||
// })
|
||||
|
||||
// // Should render with full opacity
|
||||
// expect(mockCtx.globalAlpha).toBe(1)
|
||||
// })
|
||||
|
||||
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")
|
||||
subgraph.add(node)
|
||||
|
||||
// Add subgraph output with incompatible type
|
||||
const subgraphOutput = subgraph.addOutput("result", "number")
|
||||
|
||||
// Get the string output slot from the node
|
||||
const nodeStringOutput = node.outputs[0]
|
||||
|
||||
// Draw the SubgraphOutput slot while dragging from a node output with incompatible type
|
||||
subgraphOutput.draw({
|
||||
ctx: mockCtx,
|
||||
colorContext: mockColorContext,
|
||||
fromSlot: nodeStringOutput,
|
||||
editorAlpha: 1,
|
||||
})
|
||||
|
||||
// Should render with 40% opacity due to type mismatch
|
||||
// Check that 0.4 was set during drawing
|
||||
expect(globalAlphaValues).toContain(0.4)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user