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