mores tests + refactor

This commit is contained in:
pythongosssss 2023-10-15 14:54:01 +01:00
parent 9d17f78476
commit efea06dda5
4 changed files with 235 additions and 109 deletions

View File

@ -2,6 +2,8 @@
const config = { const config = {
testEnvironment: "jsdom", testEnvironment: "jsdom",
setupFiles: ["./globalSetup.js"], setupFiles: ["./globalSetup.js"],
clearMocks: true,
resetModules: true,
}; };
module.exports = config; module.exports = config;

View File

@ -1,9 +1,49 @@
/// <reference path="../node_modules/@types/jest/index.d.ts" />
// @ts-check // @ts-check
/// <reference path="../node_modules/@types/jest/index.d.ts" />
const { start } = require("../utils"); const { start, makeNodeDef, checkBeforeAndAfterReload, assertNotNullOrUndefined } = require("../utils");
const lg = require("../utils/litegraph"); const lg = require("../utils/litegraph");
/**
* @typedef { import("../utils/ezgraph") } Ez
* @typedef { ReturnType<Ez["Ez"]["graph"]>["ez"] } EzNodeFactory
*/
/**
* @param { EzNodeFactory } ez
* @param { InstanceType<Ez["EzGraph"]> } graph
* @param { InstanceType<Ez["EzInput"]> } 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", () => { describe("widget inputs", () => {
beforeEach(() => { beforeEach(() => {
@ -12,7 +52,6 @@ describe("widget inputs", () => {
afterEach(() => { afterEach(() => {
lg.teardown(global); lg.teardown(global);
jest.resetModules();
}); });
[ [
@ -28,58 +67,27 @@ describe("widget inputs", () => {
{ name: "combo", type: ["a", "b", "c"], control: true }, { name: "combo", type: ["a", "b", "c"], control: true },
].forEach((c) => { ].forEach((c) => {
test(`widget conversion + primitive works on ${c.name}`, async () => { test(`widget conversion + primitive works on ${c.name}`, async () => {
/** const { ez, graph } = await start({
* 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({
mockNodeDefs: { mockNodeDefs: {
WidgetTestNode, ["TestNode"]: makeNodeDef("TestNode", { [c.name]: [c.type, c.opt ?? {}] }),
}, },
}); });
// Create test node and convert to input // Create test node and convert to input
const n = ez.WidgetTestNode(); const n = ez.TestNode();
const w = n.widgets[c.name]; const w = n.widgets[c.name];
w.convertToInput(); w.convertToInput();
expect(w.isConvertedToInput).toBeTruthy(); expect(w.isConvertedToInput).toBeTruthy();
const input = w.getConvertedInput(); const input = w.getConvertedInput();
expect(input).toBeTruthy(); expect(input).toBeTruthy();
// Connect to primitive // @ts-ignore : input is valid here
const p1 = ez.PrimitiveNode(); await connectPrimitiveAndReload(ez, graph, input, c.widget ?? c.name, c.control);
// @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);
}); });
}); });
test("converted widget works after reload", async () => { test("converted widget works after reload", async () => {
const { graph, ez } = await start(); const { ez, graph } = await start();
let n = ez.CheckpointLoaderSimple(); let n = ez.CheckpointLoaderSimple();
const inputCount = n.inputs.length; const inputCount = n.inputs.length;
@ -100,31 +108,14 @@ describe("widget inputs", () => {
n.widgets.ckpt_name.convertToInput(); n.widgets.ckpt_name.convertToInput();
expect(n.inputs.length).toEqual(inputCount + 1); expect(n.inputs.length).toEqual(inputCount + 1);
let primitive = ez.PrimitiveNode(); const primitive = await connectPrimitiveAndReload(ez, graph, n.inputs.ckpt_name, "combo", true);
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);
// Disconnect & reconnect // Disconnect & reconnect
connections[0].disconnect(); primitive.outputs[0].connections[0].disconnect();
({ connections } = primitive.outputs[0]); let { connections } = primitive.outputs[0];
expect(connections).toHaveLength(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]); ({ connections } = primitive.outputs[0]);
expect(connections).toHaveLength(1); expect(connections).toHaveLength(1);
expect(connections[0].targetNode.id).toBe(n.node.id); 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 () => { test("shows missing node error on custom node with converted input", async () => {
const { graph } = await start(); 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({ await graph.app.loadGraphData({
last_node_id: 3, 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("the following node types were not found");
expect(dialogShow.mock.calls[0][0]).toContain("TestNode"); expect(dialogShow.mock.calls[0][0]).toContain("TestNode");
}); });
});
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");
});
});

View File

@ -12,7 +12,7 @@
* @typedef { (...args: EzOutput[] | [...EzOutput[], Record<string, unknown>]) => EzNode } EzNodeFactory * @typedef { (...args: EzOutput[] | [...EzOutput[], Record<string, unknown>]) => EzNode } EzNodeFactory
*/ */
class EzConnection { export class EzConnection {
/** @type { app } */ /** @type { app } */
app; app;
/** @type { InstanceType<LG["LLink"]> } */ /** @type { InstanceType<LG["LLink"]> } */
@ -48,7 +48,7 @@ class EzConnection {
} }
} }
class EzSlot { export class EzSlot {
/** @type { EzNode } */ /** @type { EzNode } */
node; node;
/** @type { number } */ /** @type { number } */
@ -64,7 +64,7 @@ class EzSlot {
} }
} }
class EzInput extends EzSlot { export class EzInput extends EzSlot {
/** @type { INodeInputSlot } */ /** @type { INodeInputSlot } */
input; input;
@ -83,7 +83,7 @@ class EzInput extends EzSlot {
} }
} }
class EzOutput extends EzSlot { export class EzOutput extends EzSlot {
/** @type { INodeOutputSlot } */ /** @type { INodeOutputSlot } */
output; output;
@ -98,8 +98,9 @@ class EzOutput extends EzSlot {
} }
get connections() { get connections() {
return (this.node.node.outputs?.[this.index]?.links ?? []) return (this.node.node.outputs?.[this.index]?.links ?? []).map(
.map(l => new EzConnection(this.node.app, this.node.app.graph.links[l])); (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 } */ /** @type { EzNode } */
node; node;
/** @type { number } */
index;
/** @type { ContextMenuItem } */ /** @type { ContextMenuItem } */
item; item;
/** /**
* @param { EzNode } node * @param { EzNode } node
* @param { number } index
* @param { ContextMenuItem } item * @param { ContextMenuItem } item
*/ */
constructor(node, item) { constructor(node, index, item) {
this.node = node; this.node = node;
this.index = index;
this.item = item; this.item = item;
} }
@ -147,18 +152,22 @@ class EzNodeMenuItem {
} }
} }
class EzWidget { export class EzWidget {
/** @type { EzNode } */ /** @type { EzNode } */
node; node;
/** @type { number } */
index;
/** @type { IWidget } */ /** @type { IWidget } */
widget; widget;
/** /**
* @param { EzNode } node * @param { EzNode } node
* @param { number } index
* @param { IWidget } widget * @param { IWidget } widget
*/ */
constructor(node, widget) { constructor(node, index, widget) {
this.node = node; this.node = node;
this.index = index;
this.widget = widget; this.widget = widget;
} }
@ -176,10 +185,9 @@ class EzWidget {
} }
getConvertedInput() { getConvertedInput() {
if (!this.isConvertedToInput) if (!this.isConvertedToInput) throw new Error(`Widget ${this.widget.name} is not converted to input.`);
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);
return this.node.inputs.find(inp => inp.input["widget"]?.name === this.widget.name);
} }
convertToWidget() { convertToWidget() {
@ -195,7 +203,7 @@ class EzWidget {
} }
} }
class EzNode { export class EzNode {
/** @type { app } */ /** @type { app } */
app; app;
/** @type { LGNode } */ /** @type { LGNode } */
@ -215,55 +223,68 @@ class EzNode {
} }
get inputs() { get inputs() {
return this.#getSlotItems("inputs"); return this.#makeLookupArray("inputs", "name", EzInput);
} }
get outputs() { get outputs() {
return this.#getSlotItems("outputs"); return this.#makeLookupArray("outputs", "name", EzOutput);
} }
/** @returns { Record<string, EzWidget> } */
get widgets() { get widgets() {
return (this.node.widgets ?? []).reduce((p, w, i) => { return this.#makeLookupArray("widgets", "name", EzWidget);
p[w.name ?? i] = new EzWidget(this, w);
return p;
}, {});
} }
get menu() { get menu() {
const items = this.app.canvas.getNodeMenuOptions(this.node); return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem);
return items.reduce((p, w) => {
if(w?.content) {
p[w.content] = new EzNodeMenuItem(this, w);
}
return p;
}, {});
} }
select() { select() {
this.app.canvas.selectNode(this.node); this.app.canvas.selectNode(this.node);
} }
/** // /**
* @template { "inputs" | "outputs" } T // * @template { "inputs" | "outputs" } T
* @param { T } type // * @param { T } type
* @returns { Record<string, type extends "inputs" ? EzInput : EzOutput> & (type extends "inputs" ? EzInput [] : EzOutput[]) } // * @returns { Record<string, type extends "inputs" ? EzInput : EzOutput> & (type extends "inputs" ? EzInput [] : EzOutput[]) }
*/ // */
#getSlotItems(type) { // #getSlotItems(type) {
// @ts-ignore : these items are correct // // @ts-ignore : these items are correct
return (this.node[type] ?? []).reduce((p, s, i) => { // return (this.node[type] ?? []).reduce((p, s, i) => {
if(s.name in p) { // if (s.name in p) {
throw new Error(`Unable to store input ${s.name} on array as name conflicts.`); // 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<unknown>) } nodeProperty
* @param { string } nameProperty
* @param { T } ctor
* @returns { Record<string, InstanceType<T>> & Array<InstanceType<T>> }
*/
#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 // @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; return p;
}, Object.assign([], {$: this})) }, Object.assign([], { $: this }));
} }
} }
class EzGraph { export class EzGraph {
/** @type { app } */ /** @type { app } */
app; app;
@ -373,8 +394,7 @@ export const Ez = {
const inputs = ezNode.inputs; const inputs = ezNode.inputs;
let slot = 0; let slot = 0;
for (let i = 0; i < args.length; i++) { for (const arg of args) {
const arg = args[i];
if (arg instanceof EzOutput) { if (arg instanceof EzOutput) {
arg.connectTo(inputs[slot++]); arg.connectTo(inputs[slot++]);
} else { } else {

View File

@ -12,3 +12,45 @@ export async function start(config = undefined) {
await app.setup(); await app.setup();
return Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]); return Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]);
} }
/**
* @param { ReturnType<Ez["graph"]>["graph"] } graph
* @param { (hasReloaded: boolean) => (Promise<void> | void) } cb
*/
export async function checkBeforeAndAfterReload(graph, cb) {
await cb(false);
await graph.reload();
await cb(true);
}
/**
* @param { string } name
* @param { Record<string, string | [string | string[], any]> } 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<T, null | undefined> }
*/
export function assertNotNullOrUndefined(x) {
expect(x).not.toEqual(null);
expect(x).not.toEqual(undefined);
return true;
}