From efea06dda53edba7c60c05ae609052aa8d2fed0e Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Sun, 15 Oct 2023 14:54:01 +0100 Subject: [PATCH] mores tests + refactor --- tests-ui/jest.config.js | 2 + tests-ui/tests/widgetInputs.test.js | 188 ++++++++++++++++++---------- tests-ui/utils/ezgraph.js | 112 ++++++++++------- tests-ui/utils/index.js | 42 +++++++ 4 files changed, 235 insertions(+), 109 deletions(-) diff --git a/tests-ui/jest.config.js b/tests-ui/jest.config.js index cb5a4da18..b5a5d646d 100644 --- a/tests-ui/jest.config.js +++ b/tests-ui/jest.config.js @@ -2,6 +2,8 @@ const config = { testEnvironment: "jsdom", setupFiles: ["./globalSetup.js"], + clearMocks: true, + resetModules: true, }; module.exports = config; diff --git a/tests-ui/tests/widgetInputs.test.js b/tests-ui/tests/widgetInputs.test.js index 826c2bbb1..7fed1f28f 100644 --- a/tests-ui/tests/widgetInputs.test.js +++ b/tests-ui/tests/widgetInputs.test.js @@ -1,9 +1,49 @@ -/// // @ts-check +/// -const { start } = require("../utils"); +const { start, makeNodeDef, checkBeforeAndAfterReload, assertNotNullOrUndefined } = require("../utils"); const lg = require("../utils/litegraph"); +/** + * @typedef { import("../utils/ezgraph") } Ez + * @typedef { ReturnType["ez"] } EzNodeFactory + */ + +/** + * @param { EzNodeFactory } ez + * @param { InstanceType } graph + * @param { InstanceType } input + * @param { string } widgetType + * @param { boolean } hasControlWidget + * @returns + */ +async function connectPrimitiveAndReload(ez, graph, input, widgetType, hasControlWidget) { + // Connect to primitive and ensure its still connected after + 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); + + // Ensure widget is correct type + const valueWidget = primitive.widgets.value; + expect(valueWidget.widget.type).toBe(widgetType); + + // Check if control_after_generate should be added + if (hasControlWidget) { + const controlWidget = primitive.widgets.control_after_generate; + expect(controlWidget.widget.type).toBe("combo"); + } + + // Ensure we dont have other widgets + expect(primitive.node.widgets).toHaveLength(1 + +!!hasControlWidget); + }); + + return primitive; +} describe("widget inputs", () => { beforeEach(() => { @@ -12,7 +52,6 @@ describe("widget inputs", () => { afterEach(() => { lg.teardown(global); - jest.resetModules(); }); [ @@ -28,58 +67,27 @@ describe("widget inputs", () => { { name: "combo", type: ["a", "b", "c"], control: true }, ].forEach((c) => { test(`widget conversion + primitive works on ${c.name}`, async () => { - /** - * Test node with widgets of each type - * @type { import("../../web/types/comfy").ComfyObjectInfo } ComfyObjectInfo - */ - const WidgetTestNode = { - category: "test", - name: "WidgetTestNode", - output_name: [], - input: { - required: { - [c.name]: [c.type, c.opt ?? {}], - }, - }, - }; - - const { ez } = await start({ + const { ez, graph } = await start({ mockNodeDefs: { - WidgetTestNode, + ["TestNode"]: makeNodeDef("TestNode", { [c.name]: [c.type, c.opt ?? {}] }), }, }); // Create test node and convert to input - const n = ez.WidgetTestNode(); + const n = ez.TestNode(); const w = n.widgets[c.name]; w.convertToInput(); expect(w.isConvertedToInput).toBeTruthy(); const input = w.getConvertedInput(); expect(input).toBeTruthy(); - // Connect to primitive - const p1 = ez.PrimitiveNode(); - // @ts-ignore : input is valid - p1.outputs[0].connectTo(input); - expect(p1.outputs[0].connectTo).toHaveLength(1); - - // Ensure widget is correct type - const valueWidget = p1.widgets.value; - expect(valueWidget.widget.type).toBe(c.widget ?? c.name); - - // Check if control_after_generate should be added - if (c.control) { - const controlWidget = p1.widgets.control_after_generate; - expect(controlWidget.widget.type).toBe("combo"); - } - - // Ensure we dont have other widgets - expect(p1.node.widgets).toHaveLength(1 + +!!c.control); + // @ts-ignore : input is valid here + await connectPrimitiveAndReload(ez, graph, input, c.widget ?? c.name, c.control); }); }); test("converted widget works after reload", async () => { - const { graph, ez } = await start(); + const { ez, graph } = await start(); let n = ez.CheckpointLoaderSimple(); const inputCount = n.inputs.length; @@ -100,31 +108,14 @@ describe("widget inputs", () => { n.widgets.ckpt_name.convertToInput(); expect(n.inputs.length).toEqual(inputCount + 1); - let primitive = ez.PrimitiveNode(); - primitive.outputs[0].connectTo(n.inputs.ckpt_name); - - await graph.reload(); - - // Find the reloaded nodes in the graph - n = graph.find(n); - primitive = graph.find(primitive); - - // Ensure widget is converted - expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy(); - expect(n.inputs.ckpt_name).toBeTruthy(); - expect(n.inputs.length).toEqual(inputCount + 1); - - // Ensure primitive is connected - let { connections } = primitive.outputs[0]; - expect(connections).toHaveLength(1); - expect(connections[0].targetNode.id).toBe(n.node.id); + const primitive = await connectPrimitiveAndReload(ez, graph, n.inputs.ckpt_name, "combo", true); // Disconnect & reconnect - connections[0].disconnect(); - ({ connections } = primitive.outputs[0]); + primitive.outputs[0].connections[0].disconnect(); + let { connections } = primitive.outputs[0]; expect(connections).toHaveLength(0); - primitive.outputs[0].connectTo(n.inputs.ckpt_name); + 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); @@ -169,7 +160,7 @@ describe("widget inputs", () => { 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") + const dialogShow = jest.spyOn(graph.app.ui.dialog, "show"); await graph.app.loadGraphData({ last_node_id: 3, @@ -212,4 +203,75 @@ describe("widget inputs", () => { expect(dialogShow.mock.calls[0][0]).toContain("the following node types were not found"); expect(dialogShow.mock.calls[0][0]).toContain("TestNode"); }); -}); \ No newline at end of file + + test("defaultInput widgets can be converted back to inputs", async () => { + const { graph, ez } = await start({ + mockNodeDefs: { + ["TestNode"]: makeNodeDef("TestNode", { example: ["INT", { defaultInput: true }] }), + }, + }); + + // Create test node and ensure it starts as an input + let n = ez.TestNode(); + let w = n.widgets.example; + expect(w.isConvertedToInput).toBeTruthy(); + let input = w.getConvertedInput(); + expect(input).toBeTruthy(); + + // Ensure it can be converted to + w.convertToWidget(); + expect(w.isConvertedToInput).toBeFalsy(); + expect(n.inputs.length).toEqual(0); + // and from + w.convertToInput(); + expect(w.isConvertedToInput).toBeTruthy(); + input = w.getConvertedInput(); + + // Reload and ensure it still only has 1 converted widget + if (!assertNotNullOrUndefined(input)) return; + + await connectPrimitiveAndReload(ez, graph, input, "number", true); + n = graph.find(n); + expect(n.widgets).toHaveLength(1); + w = n.widgets.example; + expect(w.isConvertedToInput).toBeTruthy(); + + // Convert back to widget and ensure it is still a widget after reload + 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: { + ["TestNode"]: makeNodeDef("TestNode", { example: ["INT", { forceInput: true }] }), + }, + }); + + // Create test node and ensure it starts as an input + let n = ez.TestNode(); + let w = n.widgets.example; + expect(w.isConvertedToInput).toBeTruthy(); + const input = w.getConvertedInput(); + expect(input).toBeTruthy(); + + // Convert to widget should error + expect(() => w.convertToWidget()).toThrow(); + + // Reload and ensure it still only has 1 converted widget + if (assertNotNullOrUndefined(input)) { + await connectPrimitiveAndReload(ez, graph, input, "number", true); + n = graph.find(n); + expect(n.widgets).toHaveLength(1); + expect(n.widgets.example.isConvertedToInput).toBeTruthy(); + } + }); + + test("primitive combo cannot connect to non matching list", () => { + throw new Error("not implemented"); + }); +}); diff --git a/tests-ui/utils/ezgraph.js b/tests-ui/utils/ezgraph.js index fd1d2dc7f..1124ca698 100644 --- a/tests-ui/utils/ezgraph.js +++ b/tests-ui/utils/ezgraph.js @@ -12,7 +12,7 @@ * @typedef { (...args: EzOutput[] | [...EzOutput[], Record]) => EzNode } EzNodeFactory */ -class EzConnection { +export class EzConnection { /** @type { app } */ app; /** @type { InstanceType } */ @@ -48,7 +48,7 @@ class EzConnection { } } -class EzSlot { +export class EzSlot { /** @type { EzNode } */ node; /** @type { number } */ @@ -64,7 +64,7 @@ class EzSlot { } } -class EzInput extends EzSlot { +export class EzInput extends EzSlot { /** @type { INodeInputSlot } */ input; @@ -83,7 +83,7 @@ class EzInput extends EzSlot { } } -class EzOutput extends EzSlot { +export class EzOutput extends EzSlot { /** @type { INodeOutputSlot } */ output; @@ -98,8 +98,9 @@ class EzOutput extends EzSlot { } get connections() { - return (this.node.node.outputs?.[this.index]?.links ?? []) - .map(l => new EzConnection(this.node.app, this.node.app.graph.links[l])); + return (this.node.node.outputs?.[this.index]?.links ?? []).map( + (l) => new EzConnection(this.node.app, this.node.app.graph.links[l]) + ); } /** @@ -123,18 +124,22 @@ class EzOutput extends EzSlot { } } -class EzNodeMenuItem { +export class EzNodeMenuItem { /** @type { EzNode } */ node; + /** @type { number } */ + index; /** @type { ContextMenuItem } */ item; /** * @param { EzNode } node + * @param { number } index * @param { ContextMenuItem } item */ - constructor(node, item) { + constructor(node, index, item) { this.node = node; + this.index = index; this.item = item; } @@ -147,18 +152,22 @@ class EzNodeMenuItem { } } -class EzWidget { +export class EzWidget { /** @type { EzNode } */ node; + /** @type { number } */ + index; /** @type { IWidget } */ widget; /** * @param { EzNode } node + * @param { number } index * @param { IWidget } widget */ - constructor(node, widget) { + constructor(node, index, widget) { this.node = node; + this.index = index; this.widget = widget; } @@ -176,10 +185,9 @@ class EzWidget { } getConvertedInput() { - if (!this.isConvertedToInput) - throw new Error(`Widget ${this.widget.name} is not converted to input.`); - - return this.node.inputs.find(inp => inp.input["widget"]?.name === this.widget.name); + if (!this.isConvertedToInput) throw new Error(`Widget ${this.widget.name} is not converted to input.`); + + return this.node.inputs.find((inp) => inp.input["widget"]?.name === this.widget.name); } convertToWidget() { @@ -195,7 +203,7 @@ class EzWidget { } } -class EzNode { +export class EzNode { /** @type { app } */ app; /** @type { LGNode } */ @@ -215,55 +223,68 @@ class EzNode { } get inputs() { - return this.#getSlotItems("inputs"); + return this.#makeLookupArray("inputs", "name", EzInput); } get outputs() { - return this.#getSlotItems("outputs"); + return this.#makeLookupArray("outputs", "name", EzOutput); } - /** @returns { Record } */ get widgets() { - return (this.node.widgets ?? []).reduce((p, w, i) => { - p[w.name ?? i] = new EzWidget(this, w); - return p; - }, {}); + return this.#makeLookupArray("widgets", "name", EzWidget); } get menu() { - const items = this.app.canvas.getNodeMenuOptions(this.node); - return items.reduce((p, w) => { - if(w?.content) { - p[w.content] = new EzNodeMenuItem(this, w); - } - return p; - }, {}); + return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem); } select() { this.app.canvas.selectNode(this.node); } - /** - * @template { "inputs" | "outputs" } T - * @param { T } type - * @returns { Record & (type extends "inputs" ? EzInput [] : EzOutput[]) } - */ - #getSlotItems(type) { - // @ts-ignore : these items are correct - return (this.node[type] ?? []).reduce((p, s, i) => { - if(s.name in p) { - throw new Error(`Unable to store input ${s.name} on array as name conflicts.`); - } - ; + // /** + // * @template { "inputs" | "outputs" } T + // * @param { T } type + // * @returns { Record & (type extends "inputs" ? EzInput [] : EzOutput[]) } + // */ + // #getSlotItems(type) { + // // @ts-ignore : these items are correct + // return (this.node[type] ?? []).reduce((p, s, i) => { + // if (s.name in p) { + // throw new Error(`Unable to store input ${s.name} on array as name conflicts.`); + // } + // // @ts-ignore + // p.push((p[s.name] = new (type === "inputs" ? EzInput : EzOutput)(this, i, s))); + // return p; + // }, Object.assign([], { $: this })); + // } + + /** + * @template { { new(node: EzNode, index: number, obj: any): any } } T + * @param { "inputs" | "outputs" | "widgets" | (() => Array) } nodeProperty + * @param { string } nameProperty + * @param { T } ctor + * @returns { Record> & Array> } + */ + #makeLookupArray(nodeProperty, nameProperty, ctor) { + const items = typeof nodeProperty === "function" ? nodeProperty() : this.node[nodeProperty]; + // @ts-ignore + return (items ?? []).reduce((p, s, i) => { + if (!s) return p; + + const name = s[nameProperty]; // @ts-ignore - p.push(p[s.name] = new (type === "inputs" ? EzInput : EzOutput)(this, i, s)); + if (!name || name in p) { + throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`); + } + // @ts-ignore + p.push((p[name] = new ctor(this, i, s))); return p; - }, Object.assign([], {$: this})) + }, Object.assign([], { $: this })); } } -class EzGraph { +export class EzGraph { /** @type { app } */ app; @@ -373,8 +394,7 @@ export const Ez = { const inputs = ezNode.inputs; let slot = 0; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; + for (const arg of args) { if (arg instanceof EzOutput) { arg.connectTo(inputs[slot++]); } else { diff --git a/tests-ui/utils/index.js b/tests-ui/utils/index.js index 33d0ac025..50399a7ef 100644 --- a/tests-ui/utils/index.js +++ b/tests-ui/utils/index.js @@ -12,3 +12,45 @@ export async function start(config = undefined) { await app.setup(); return Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]); } + +/** + * @param { ReturnType["graph"] } graph + * @param { (hasReloaded: boolean) => (Promise | void) } cb + */ +export async function checkBeforeAndAfterReload(graph, cb) { + await cb(false); + await graph.reload(); + await cb(true); +} + +/** + * @param { string } name + * @param { Record } input + * @returns { import("../../web/types/comfy").ComfyObjectInfo } + */ +export function makeNodeDef(name, input) { + const nodeDef = { + name, + category: "test", + output_name: [], + input: { + required: {} + }, + }; + for(const k in input) { + nodeDef.input.required[k] = typeof input[k] === "string" ? [input[k], {}] : [...input[k]]; + } + return nodeDef; +} + +/** +/** + * @template { any } T + * @param { T } x + * @returns { x is Exclude } + */ +export function assertNotNullOrUndefined(x) { + expect(x).not.toEqual(null); + expect(x).not.toEqual(undefined); + return true; +} \ No newline at end of file