| | |
| | |
| |
|
| | const { |
| | start, |
| | makeNodeDef, |
| | checkBeforeAndAfterReload, |
| | assertNotNullOrUndefined, |
| | createDefaultWorkflow, |
| | } = require("../utils"); |
| | const lg = require("../utils/litegraph"); |
| |
|
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | async function connectPrimitiveAndReload(ez, graph, input, widgetType, controlWidgetCount = 0) { |
| | |
| | let primitive = ez.PrimitiveNode(); |
| | primitive.outputs[0].connectTo(input); |
| |
|
| | await checkBeforeAndAfterReload(graph, async () => { |
| | primitive = graph.find(primitive); |
| | let { connections } = primitive.outputs[0]; |
| | expect(connections).toHaveLength(1); |
| | expect(connections[0].targetNode.id).toBe(input.node.node.id); |
| |
|
| | |
| | const valueWidget = primitive.widgets.value; |
| | expect(valueWidget.widget.type).toBe(widgetType); |
| |
|
| | |
| | if (controlWidgetCount) { |
| | const controlWidget = primitive.widgets.control_after_generate; |
| | expect(controlWidget.widget.type).toBe("combo"); |
| | if (widgetType === "combo") { |
| | const filterWidget = primitive.widgets.control_filter_list; |
| | expect(filterWidget.widget.type).toBe("string"); |
| | } |
| | } |
| |
|
| | |
| | expect(primitive.node.widgets).toHaveLength(1 + controlWidgetCount); |
| | }); |
| |
|
| | return primitive; |
| | } |
| |
|
| | describe("widget inputs", () => { |
| | beforeEach(() => { |
| | lg.setup(global); |
| | }); |
| |
|
| | afterEach(() => { |
| | lg.teardown(global); |
| | }); |
| |
|
| | [ |
| | { name: "int", type: "INT", widget: "number", control: 1 }, |
| | { name: "float", type: "FLOAT", widget: "number", control: 1 }, |
| | { name: "text", type: "STRING" }, |
| | { |
| | name: "customtext", |
| | type: "STRING", |
| | opt: { multiline: true }, |
| | }, |
| | { name: "toggle", type: "BOOLEAN" }, |
| | { name: "combo", type: ["a", "b", "c"], control: 2 }, |
| | ].forEach((c) => { |
| | test(`widget conversion + primitive works on ${c.name}`, async () => { |
| | const { ez, graph } = await start({ |
| | mockNodeDefs: makeNodeDef("TestNode", { [c.name]: [c.type, c.opt ?? {}] }), |
| | }); |
| |
|
| | |
| | const n = ez.TestNode(); |
| | const w = n.widgets[c.name]; |
| | w.convertToInput(); |
| | expect(w.isConvertedToInput).toBeTruthy(); |
| | const input = w.getConvertedInput(); |
| | expect(input).toBeTruthy(); |
| |
|
| | |
| | await connectPrimitiveAndReload(ez, graph, input, c.widget ?? c.name, c.control); |
| | }); |
| | }); |
| |
|
| | test("converted widget works after reload", async () => { |
| | const { ez, graph } = await start(); |
| | let n = ez.CheckpointLoaderSimple(); |
| |
|
| | const inputCount = n.inputs.length; |
| |
|
| | |
| | n.widgets.ckpt_name.convertToInput(); |
| | expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy(); |
| | expect(n.inputs.ckpt_name).toBeTruthy(); |
| | expect(n.inputs.length).toEqual(inputCount + 1); |
| |
|
| | |
| | n.widgets.ckpt_name.convertToWidget(); |
| | expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy(); |
| | expect(n.inputs.ckpt_name).toBeFalsy(); |
| | expect(n.inputs.length).toEqual(inputCount); |
| |
|
| | |
| | n.widgets.ckpt_name.convertToInput(); |
| | expect(n.inputs.length).toEqual(inputCount + 1); |
| |
|
| | const primitive = await connectPrimitiveAndReload(ez, graph, n.inputs.ckpt_name, "combo", 2); |
| |
|
| | |
| | primitive.outputs[0].connections[0].disconnect(); |
| | let { connections } = primitive.outputs[0]; |
| | expect(connections).toHaveLength(0); |
| |
|
| | primitive.outputs[0].connectTo(n.inputs.ckpt_name); |
| | ({ connections } = primitive.outputs[0]); |
| | expect(connections).toHaveLength(1); |
| | expect(connections[0].targetNode.id).toBe(n.node.id); |
| |
|
| | |
| | n.widgets.ckpt_name.convertToWidget(); |
| | expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy(); |
| | expect(n.inputs.ckpt_name).toBeFalsy(); |
| | expect(n.inputs.length).toEqual(inputCount); |
| | }); |
| |
|
| | test("converted widget works on clone", async () => { |
| | const { graph, ez } = await start(); |
| | let n = ez.CheckpointLoaderSimple(); |
| |
|
| | |
| | n.widgets.ckpt_name.convertToInput(); |
| | expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy(); |
| |
|
| | |
| | n.menu["Clone"].call(); |
| | expect(graph.nodes).toHaveLength(2); |
| | const clone = graph.nodes[1]; |
| | expect(clone.id).not.toEqual(n.id); |
| |
|
| | |
| | expect(clone.widgets.ckpt_name.isConvertedToInput).toBeTruthy(); |
| | expect(clone.inputs.ckpt_name).toBeTruthy(); |
| |
|
| | |
| | let primitive = ez.PrimitiveNode(); |
| | primitive.outputs[0].connectTo(n.inputs.ckpt_name); |
| | primitive.outputs[0].connectTo(clone.inputs.ckpt_name); |
| | expect(primitive.outputs[0].connections).toHaveLength(2); |
| |
|
| | |
| | clone.widgets.ckpt_name.convertToWidget(); |
| | expect(clone.widgets.ckpt_name.isConvertedToInput).toBeFalsy(); |
| | expect(clone.inputs.ckpt_name).toBeFalsy(); |
| | }); |
| |
|
| | test("shows missing node error on custom node with converted input", async () => { |
| | const { graph } = await start(); |
| |
|
| | const dialogShow = jest.spyOn(graph.app.ui.dialog, "show"); |
| |
|
| | await graph.app.loadGraphData({ |
| | last_node_id: 3, |
| | last_link_id: 4, |
| | nodes: [ |
| | { |
| | id: 1, |
| | type: "TestNode", |
| | pos: [41.87329101561909, 389.7381480823742], |
| | size: { 0: 220, 1: 374 }, |
| | flags: {}, |
| | order: 1, |
| | mode: 0, |
| | inputs: [{ name: "test", type: "FLOAT", link: 4, widget: { name: "test" }, slot_index: 0 }], |
| | outputs: [], |
| | properties: { "Node name for S&R": "TestNode" }, |
| | widgets_values: [1], |
| | }, |
| | { |
| | id: 3, |
| | type: "PrimitiveNode", |
| | pos: [-312, 433], |
| | size: { 0: 210, 1: 82 }, |
| | flags: {}, |
| | order: 0, |
| | mode: 0, |
| | outputs: [{ links: [4], widget: { name: "test" } }], |
| | title: "test", |
| | properties: {}, |
| | }, |
| | ], |
| | links: [[4, 3, 0, 1, 6, "FLOAT"]], |
| | groups: [], |
| | config: {}, |
| | extra: {}, |
| | version: 0.4, |
| | }); |
| |
|
| | expect(dialogShow).toBeCalledTimes(1); |
| | expect(dialogShow.mock.calls[0][0].innerHTML).toContain("the following node types were not found"); |
| | expect(dialogShow.mock.calls[0][0].innerHTML).toContain("TestNode"); |
| | }); |
| |
|
| | test("defaultInput widgets can be converted back to inputs", async () => { |
| | const { graph, ez } = await start({ |
| | mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { defaultInput: true }] }), |
| | }); |
| |
|
| | |
| | let n = ez.TestNode(); |
| | let w = n.widgets.example; |
| | expect(w.isConvertedToInput).toBeTruthy(); |
| | let input = w.getConvertedInput(); |
| | expect(input).toBeTruthy(); |
| |
|
| | |
| | w.convertToWidget(); |
| | expect(w.isConvertedToInput).toBeFalsy(); |
| | expect(n.inputs.length).toEqual(0); |
| | |
| | w.convertToInput(); |
| | expect(w.isConvertedToInput).toBeTruthy(); |
| | input = w.getConvertedInput(); |
| |
|
| | |
| | if (!assertNotNullOrUndefined(input)) return; |
| |
|
| | await connectPrimitiveAndReload(ez, graph, input, "number", 1); |
| | n = graph.find(n); |
| | expect(n.widgets).toHaveLength(1); |
| | w = n.widgets.example; |
| | expect(w.isConvertedToInput).toBeTruthy(); |
| |
|
| | |
| | w.convertToWidget(); |
| | await graph.reload(); |
| | n = graph.find(n); |
| | expect(n.widgets).toHaveLength(1); |
| | expect(n.widgets[0].isConvertedToInput).toBeFalsy(); |
| | expect(n.inputs.length).toEqual(0); |
| | }); |
| |
|
| | test("forceInput widgets can not be converted back to inputs", async () => { |
| | const { graph, ez } = await start({ |
| | mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { forceInput: true }] }), |
| | }); |
| |
|
| | |
| | let n = ez.TestNode(); |
| | let w = n.widgets.example; |
| | expect(w.isConvertedToInput).toBeTruthy(); |
| | const input = w.getConvertedInput(); |
| | expect(input).toBeTruthy(); |
| |
|
| | |
| | expect(() => w.convertToWidget()).toThrow(); |
| |
|
| | |
| | if (assertNotNullOrUndefined(input)) { |
| | await connectPrimitiveAndReload(ez, graph, input, "number", 1); |
| | n = graph.find(n); |
| | expect(n.widgets).toHaveLength(1); |
| | expect(n.widgets.example.isConvertedToInput).toBeTruthy(); |
| | } |
| | }); |
| |
|
| | test("primitive can connect to matching combos on converted widgets", async () => { |
| | const { ez } = await start({ |
| | mockNodeDefs: { |
| | ...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }), |
| | ...makeNodeDef("TestNode2", { example: [["A", "B", "C"], { forceInput: true }] }), |
| | }, |
| | }); |
| |
|
| | const n1 = ez.TestNode1(); |
| | const n2 = ez.TestNode2(); |
| | const p = ez.PrimitiveNode(); |
| | p.outputs[0].connectTo(n1.inputs[0]); |
| | p.outputs[0].connectTo(n2.inputs[0]); |
| | expect(p.outputs[0].connections).toHaveLength(2); |
| | const valueWidget = p.widgets.value; |
| | expect(valueWidget.widget.type).toBe("combo"); |
| | expect(valueWidget.widget.options.values).toEqual(["A", "B", "C"]); |
| | }); |
| |
|
| | test("primitive can not connect to non matching combos on converted widgets", async () => { |
| | const { ez } = await start({ |
| | mockNodeDefs: { |
| | ...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }), |
| | ...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }), |
| | }, |
| | }); |
| |
|
| | const n1 = ez.TestNode1(); |
| | const n2 = ez.TestNode2(); |
| | const p = ez.PrimitiveNode(); |
| | p.outputs[0].connectTo(n1.inputs[0]); |
| | expect(() => p.outputs[0].connectTo(n2.inputs[0])).toThrow(); |
| | expect(p.outputs[0].connections).toHaveLength(1); |
| | }); |
| |
|
| | test("combo output can not connect to non matching combos list input", async () => { |
| | const { ez } = await start({ |
| | mockNodeDefs: { |
| | ...makeNodeDef("TestNode1", {}, [["A", "B"]]), |
| | ...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }), |
| | ...makeNodeDef("TestNode3", { example: [["A", "B", "C"], { forceInput: true }] }), |
| | }, |
| | }); |
| |
|
| | const n1 = ez.TestNode1(); |
| | const n2 = ez.TestNode2(); |
| | const n3 = ez.TestNode3(); |
| |
|
| | n1.outputs[0].connectTo(n2.inputs[0]); |
| | expect(() => n1.outputs[0].connectTo(n3.inputs[0])).toThrow(); |
| | }); |
| |
|
| | test("combo primitive can filter list when control_after_generate called", async () => { |
| | const { ez } = await start({ |
| | mockNodeDefs: { |
| | ...makeNodeDef("TestNode1", { example: [["A", "B", "C", "D", "AA", "BB", "CC", "DD", "AAA", "BBB"], {}] }), |
| | }, |
| | }); |
| |
|
| | const n1 = ez.TestNode1(); |
| | n1.widgets.example.convertToInput(); |
| | const p = ez.PrimitiveNode(); |
| | p.outputs[0].connectTo(n1.inputs[0]); |
| |
|
| | const value = p.widgets.value; |
| | const control = p.widgets.control_after_generate.widget; |
| | const filter = p.widgets.control_filter_list; |
| |
|
| | expect(p.widgets.length).toBe(3); |
| | control.value = "increment"; |
| | expect(value.value).toBe("A"); |
| |
|
| | |
| | control["afterQueued"](); |
| | expect(value.value).toBe("B"); |
| |
|
| | |
| | filter.value = "D"; |
| | control["afterQueued"](); |
| | expect(value.value).toBe("D"); |
| | control["afterQueued"](); |
| | expect(value.value).toBe("DD"); |
| |
|
| | |
| | value.value = "BBB"; |
| | control.value = "decrement"; |
| | filter.value = "B"; |
| | control["afterQueued"](); |
| | expect(value.value).toBe("BB"); |
| | control["afterQueued"](); |
| | expect(value.value).toBe("B"); |
| |
|
| | |
| | value.value = "BBB"; |
| | filter.value = "/[AB]|^C$/"; |
| | control["afterQueued"](); |
| | expect(value.value).toBe("AAA"); |
| | control["afterQueued"](); |
| | expect(value.value).toBe("BB"); |
| | control["afterQueued"](); |
| | expect(value.value).toBe("AA"); |
| | control["afterQueued"](); |
| | expect(value.value).toBe("C"); |
| | control["afterQueued"](); |
| | expect(value.value).toBe("B"); |
| | control["afterQueued"](); |
| | expect(value.value).toBe("A"); |
| |
|
| | |
| | control.value = "randomize"; |
| | filter.value = "/D/"; |
| | for (let i = 0; i < 100; i++) { |
| | control["afterQueued"](); |
| | expect(value.value === "D" || value.value === "DD").toBeTruthy(); |
| | } |
| |
|
| | |
| | control.value = "fixed"; |
| | value.value = "B"; |
| | filter.value = "C"; |
| | control["afterQueued"](); |
| | expect(value.value).toBe("B"); |
| | }); |
| |
|
| | describe("reroutes", () => { |
| | async function checkOutput(graph, values) { |
| | expect((await graph.toPrompt()).output).toStrictEqual({ |
| | 1: { inputs: { ckpt_name: "model1.safetensors" }, class_type: "CheckpointLoaderSimple" }, |
| | 2: { inputs: { text: "positive", clip: ["1", 1] }, class_type: "CLIPTextEncode" }, |
| | 3: { inputs: { text: "negative", clip: ["1", 1] }, class_type: "CLIPTextEncode" }, |
| | 4: { |
| | inputs: { width: values.width ?? 512, height: values.height ?? 512, batch_size: values?.batch_size ?? 1 }, |
| | class_type: "EmptyLatentImage", |
| | }, |
| | 5: { |
| | inputs: { |
| | seed: 0, |
| | steps: 20, |
| | cfg: 8, |
| | sampler_name: "euler", |
| | scheduler: values?.scheduler ?? "normal", |
| | denoise: 1, |
| | model: ["1", 0], |
| | positive: ["2", 0], |
| | negative: ["3", 0], |
| | latent_image: ["4", 0], |
| | }, |
| | class_type: "KSampler", |
| | }, |
| | 6: { inputs: { samples: ["5", 0], vae: ["1", 2] }, class_type: "VAEDecode" }, |
| | 7: { |
| | inputs: { filename_prefix: values.filename_prefix ?? "ComfyUI", images: ["6", 0] }, |
| | class_type: "SaveImage", |
| | }, |
| | }); |
| | } |
| |
|
| | async function waitForWidget(node) { |
| | |
| | |
| | for (let i = 0; i < 10; i++) { |
| | await new Promise((r) => setTimeout(r, 10)); |
| | if (node.widgets?.value) { |
| | return; |
| | } |
| | } |
| | } |
| |
|
| | it("can connect primitive via a reroute path to a widget input", async () => { |
| | const { ez, graph } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| |
|
| | nodes.empty.widgets.width.convertToInput(); |
| | nodes.sampler.widgets.scheduler.convertToInput(); |
| | nodes.save.widgets.filename_prefix.convertToInput(); |
| |
|
| | let widthReroute = ez.Reroute(); |
| | let schedulerReroute = ez.Reroute(); |
| | let fileReroute = ez.Reroute(); |
| |
|
| | let widthNext = widthReroute; |
| | let schedulerNext = schedulerReroute; |
| | let fileNext = fileReroute; |
| |
|
| | for (let i = 0; i < 5; i++) { |
| | let next = ez.Reroute(); |
| | widthNext.outputs[0].connectTo(next.inputs[0]); |
| | widthNext = next; |
| |
|
| | next = ez.Reroute(); |
| | schedulerNext.outputs[0].connectTo(next.inputs[0]); |
| | schedulerNext = next; |
| |
|
| | next = ez.Reroute(); |
| | fileNext.outputs[0].connectTo(next.inputs[0]); |
| | fileNext = next; |
| | } |
| |
|
| | widthNext.outputs[0].connectTo(nodes.empty.inputs.width); |
| | schedulerNext.outputs[0].connectTo(nodes.sampler.inputs.scheduler); |
| | fileNext.outputs[0].connectTo(nodes.save.inputs.filename_prefix); |
| |
|
| | let widthPrimitive = ez.PrimitiveNode(); |
| | let schedulerPrimitive = ez.PrimitiveNode(); |
| | let filePrimitive = ez.PrimitiveNode(); |
| |
|
| | widthPrimitive.outputs[0].connectTo(widthReroute.inputs[0]); |
| | schedulerPrimitive.outputs[0].connectTo(schedulerReroute.inputs[0]); |
| | filePrimitive.outputs[0].connectTo(fileReroute.inputs[0]); |
| | expect(widthPrimitive.widgets.value.value).toBe(512); |
| | widthPrimitive.widgets.value.value = 1024; |
| | expect(schedulerPrimitive.widgets.value.value).toBe("normal"); |
| | schedulerPrimitive.widgets.value.value = "simple"; |
| | expect(filePrimitive.widgets.value.value).toBe("ComfyUI"); |
| | filePrimitive.widgets.value.value = "ComfyTest"; |
| |
|
| | await checkBeforeAndAfterReload(graph, async () => { |
| | widthPrimitive = graph.find(widthPrimitive); |
| | schedulerPrimitive = graph.find(schedulerPrimitive); |
| | filePrimitive = graph.find(filePrimitive); |
| | await waitForWidget(filePrimitive); |
| | expect(widthPrimitive.widgets.length).toBe(2); |
| | expect(schedulerPrimitive.widgets.length).toBe(3); |
| | expect(filePrimitive.widgets.length).toBe(1); |
| |
|
| | await checkOutput(graph, { |
| | width: 1024, |
| | scheduler: "simple", |
| | filename_prefix: "ComfyTest", |
| | }); |
| | }); |
| | }); |
| | it("can connect primitive via a reroute path to multiple widget inputs", async () => { |
| | const { ez, graph } = await start(); |
| | const nodes = createDefaultWorkflow(ez, graph); |
| |
|
| | nodes.empty.widgets.width.convertToInput(); |
| | nodes.empty.widgets.height.convertToInput(); |
| | nodes.empty.widgets.batch_size.convertToInput(); |
| |
|
| | let reroute = ez.Reroute(); |
| | let prevReroute = reroute; |
| | for (let i = 0; i < 5; i++) { |
| | const next = ez.Reroute(); |
| | prevReroute.outputs[0].connectTo(next.inputs[0]); |
| | prevReroute = next; |
| | } |
| |
|
| | const r1 = ez.Reroute(prevReroute.outputs[0]); |
| | const r2 = ez.Reroute(prevReroute.outputs[0]); |
| | const r3 = ez.Reroute(r2.outputs[0]); |
| | const r4 = ez.Reroute(r2.outputs[0]); |
| |
|
| | r1.outputs[0].connectTo(nodes.empty.inputs.width); |
| | r3.outputs[0].connectTo(nodes.empty.inputs.height); |
| | r4.outputs[0].connectTo(nodes.empty.inputs.batch_size); |
| |
|
| | let primitive = ez.PrimitiveNode(); |
| | primitive.outputs[0].connectTo(reroute.inputs[0]); |
| | expect(primitive.widgets.value.value).toBe(1); |
| | primitive.widgets.value.value = 64; |
| |
|
| | await checkBeforeAndAfterReload(graph, async (r) => { |
| | primitive = graph.find(primitive); |
| | await waitForWidget(primitive); |
| |
|
| | |
| | expect(primitive.widgets.value.widget.options?.min).toBe(16); |
| | expect(primitive.widgets.value.widget.options?.max).toBe(4096); |
| | expect(primitive.widgets.value.widget.options?.step).toBe(80); |
| |
|
| | await checkOutput(graph, { |
| | width: 64, |
| | height: 64, |
| | batch_size: 64, |
| | }); |
| | }); |
| | }); |
| | }); |
| | }); |
| |
|