Upload folder using huggingface_hub
Browse files- ComfyUI/tests-ui/.gitignore +1 -0
- ComfyUI/tests-ui/afterSetup.js +9 -0
- ComfyUI/tests-ui/babel.config.json +4 -0
- ComfyUI/tests-ui/globalSetup.js +14 -0
- ComfyUI/tests-ui/jest.config.js +11 -0
- ComfyUI/tests-ui/package-lock.json +0 -0
- ComfyUI/tests-ui/package.json +31 -0
- ComfyUI/tests-ui/setup.js +88 -0
- ComfyUI/tests-ui/tests/extensions.test.js +196 -0
- ComfyUI/tests-ui/tests/groupNode.test.js +1005 -0
- ComfyUI/tests-ui/tests/users.test.js +295 -0
- ComfyUI/tests-ui/tests/widgetInputs.test.js +557 -0
- ComfyUI/tests-ui/utils/ezgraph.js +452 -0
- ComfyUI/tests-ui/utils/index.js +129 -0
- ComfyUI/tests-ui/utils/litegraph.js +36 -0
- ComfyUI/tests-ui/utils/nopProxy.js +6 -0
- ComfyUI/tests-ui/utils/setup.js +81 -0
ComfyUI/tests-ui/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
ComfyUI/tests-ui/afterSetup.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { start } = require("./utils");
|
| 2 |
+
const lg = require("./utils/litegraph");
|
| 3 |
+
|
| 4 |
+
// Load things once per test file before to ensure its all warmed up for the tests
|
| 5 |
+
beforeAll(async () => {
|
| 6 |
+
lg.setup(global);
|
| 7 |
+
await start({ resetEnv: true });
|
| 8 |
+
lg.teardown(global);
|
| 9 |
+
});
|
ComfyUI/tests-ui/babel.config.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"presets": ["@babel/preset-env"],
|
| 3 |
+
"plugins": ["babel-plugin-transform-import-meta"]
|
| 4 |
+
}
|
ComfyUI/tests-ui/globalSetup.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = async function () {
|
| 2 |
+
global.ResizeObserver = class ResizeObserver {
|
| 3 |
+
observe() {}
|
| 4 |
+
unobserve() {}
|
| 5 |
+
disconnect() {}
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
const { nop } = require("./utils/nopProxy");
|
| 9 |
+
global.enableWebGLCanvas = nop;
|
| 10 |
+
|
| 11 |
+
HTMLCanvasElement.prototype.getContext = nop;
|
| 12 |
+
|
| 13 |
+
localStorage["Comfy.Settings.Comfy.Logging.Enabled"] = "false";
|
| 14 |
+
};
|
ComfyUI/tests-ui/jest.config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('jest').Config} */
|
| 2 |
+
const config = {
|
| 3 |
+
testEnvironment: "jsdom",
|
| 4 |
+
setupFiles: ["./globalSetup.js"],
|
| 5 |
+
setupFilesAfterEnv: ["./afterSetup.js"],
|
| 6 |
+
clearMocks: true,
|
| 7 |
+
resetModules: true,
|
| 8 |
+
testTimeout: 10000
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
module.exports = config;
|
ComfyUI/tests-ui/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
ComfyUI/tests-ui/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "comfui-tests",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "UI tests",
|
| 5 |
+
"main": "index.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"test": "jest",
|
| 8 |
+
"test:generate": "node setup.js"
|
| 9 |
+
},
|
| 10 |
+
"repository": {
|
| 11 |
+
"type": "git",
|
| 12 |
+
"url": "git+https://github.com/comfyanonymous/ComfyUI.git"
|
| 13 |
+
},
|
| 14 |
+
"keywords": [
|
| 15 |
+
"comfyui",
|
| 16 |
+
"test"
|
| 17 |
+
],
|
| 18 |
+
"author": "comfyanonymous",
|
| 19 |
+
"license": "GPL-3.0",
|
| 20 |
+
"bugs": {
|
| 21 |
+
"url": "https://github.com/comfyanonymous/ComfyUI/issues"
|
| 22 |
+
},
|
| 23 |
+
"homepage": "https://github.com/comfyanonymous/ComfyUI#readme",
|
| 24 |
+
"devDependencies": {
|
| 25 |
+
"@babel/preset-env": "^7.22.20",
|
| 26 |
+
"@types/jest": "^29.5.5",
|
| 27 |
+
"babel-plugin-transform-import-meta": "^2.2.1",
|
| 28 |
+
"jest": "^29.7.0",
|
| 29 |
+
"jest-environment-jsdom": "^29.7.0"
|
| 30 |
+
}
|
| 31 |
+
}
|
ComfyUI/tests-ui/setup.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { spawn } = require("child_process");
|
| 2 |
+
const { resolve } = require("path");
|
| 3 |
+
const { existsSync, mkdirSync, writeFileSync } = require("fs");
|
| 4 |
+
const http = require("http");
|
| 5 |
+
|
| 6 |
+
async function setup() {
|
| 7 |
+
// Wait up to 30s for it to start
|
| 8 |
+
let success = false;
|
| 9 |
+
let child;
|
| 10 |
+
for (let i = 0; i < 30; i++) {
|
| 11 |
+
try {
|
| 12 |
+
await new Promise((res, rej) => {
|
| 13 |
+
http
|
| 14 |
+
.get("http://127.0.0.1:8188/object_info", (resp) => {
|
| 15 |
+
let data = "";
|
| 16 |
+
resp.on("data", (chunk) => {
|
| 17 |
+
data += chunk;
|
| 18 |
+
});
|
| 19 |
+
resp.on("end", () => {
|
| 20 |
+
// Modify the response data to add some checkpoints
|
| 21 |
+
const objectInfo = JSON.parse(data);
|
| 22 |
+
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name[0] = ["model1.safetensors", "model2.ckpt"];
|
| 23 |
+
objectInfo.VAELoader.input.required.vae_name[0] = ["vae1.safetensors", "vae2.ckpt"];
|
| 24 |
+
|
| 25 |
+
data = JSON.stringify(objectInfo, undefined, "\t");
|
| 26 |
+
|
| 27 |
+
const outDir = resolve("./data");
|
| 28 |
+
if (!existsSync(outDir)) {
|
| 29 |
+
mkdirSync(outDir);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const outPath = resolve(outDir, "object_info.json");
|
| 33 |
+
console.log(`Writing ${Object.keys(objectInfo).length} nodes to ${outPath}`);
|
| 34 |
+
writeFileSync(outPath, data, {
|
| 35 |
+
encoding: "utf8",
|
| 36 |
+
});
|
| 37 |
+
res();
|
| 38 |
+
});
|
| 39 |
+
})
|
| 40 |
+
.on("error", rej);
|
| 41 |
+
});
|
| 42 |
+
success = true;
|
| 43 |
+
break;
|
| 44 |
+
} catch (error) {
|
| 45 |
+
console.log(i + "/30", error);
|
| 46 |
+
if (i === 0) {
|
| 47 |
+
// Start the server on first iteration if it fails to connect
|
| 48 |
+
console.log("Starting ComfyUI server...");
|
| 49 |
+
|
| 50 |
+
let python = resolve("../../python_embeded/python.exe");
|
| 51 |
+
let args;
|
| 52 |
+
let cwd;
|
| 53 |
+
if (existsSync(python)) {
|
| 54 |
+
args = ["-s", "ComfyUI/main.py"];
|
| 55 |
+
cwd = "../..";
|
| 56 |
+
} else {
|
| 57 |
+
python = "python";
|
| 58 |
+
args = ["main.py"];
|
| 59 |
+
cwd = "..";
|
| 60 |
+
}
|
| 61 |
+
args.push("--cpu");
|
| 62 |
+
console.log(python, ...args);
|
| 63 |
+
child = spawn(python, args, { cwd });
|
| 64 |
+
child.on("error", (err) => {
|
| 65 |
+
console.log(`Server error (${err})`);
|
| 66 |
+
i = 30;
|
| 67 |
+
});
|
| 68 |
+
child.on("exit", (code) => {
|
| 69 |
+
if (!success) {
|
| 70 |
+
console.log(`Server exited (${code})`);
|
| 71 |
+
i = 30;
|
| 72 |
+
}
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
await new Promise((r) => {
|
| 76 |
+
setTimeout(r, 1000);
|
| 77 |
+
});
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
child?.kill();
|
| 82 |
+
|
| 83 |
+
if (!success) {
|
| 84 |
+
throw new Error("Waiting for server failed...");
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
setup();
|
ComfyUI/tests-ui/tests/extensions.test.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
/// <reference path="../node_modules/@types/jest/index.d.ts" />
|
| 3 |
+
const { start } = require("../utils");
|
| 4 |
+
const lg = require("../utils/litegraph");
|
| 5 |
+
|
| 6 |
+
describe("extensions", () => {
|
| 7 |
+
beforeEach(() => {
|
| 8 |
+
lg.setup(global);
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
afterEach(() => {
|
| 12 |
+
lg.teardown(global);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it("calls each extension hook", async () => {
|
| 16 |
+
const mockExtension = {
|
| 17 |
+
name: "TestExtension",
|
| 18 |
+
init: jest.fn(),
|
| 19 |
+
setup: jest.fn(),
|
| 20 |
+
addCustomNodeDefs: jest.fn(),
|
| 21 |
+
getCustomWidgets: jest.fn(),
|
| 22 |
+
beforeRegisterNodeDef: jest.fn(),
|
| 23 |
+
registerCustomNodes: jest.fn(),
|
| 24 |
+
loadedGraphNode: jest.fn(),
|
| 25 |
+
nodeCreated: jest.fn(),
|
| 26 |
+
beforeConfigureGraph: jest.fn(),
|
| 27 |
+
afterConfigureGraph: jest.fn(),
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
const { app, ez, graph } = await start({
|
| 31 |
+
async preSetup(app) {
|
| 32 |
+
app.registerExtension(mockExtension);
|
| 33 |
+
},
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
// Basic initialisation hooks should be called once, with app
|
| 37 |
+
expect(mockExtension.init).toHaveBeenCalledTimes(1);
|
| 38 |
+
expect(mockExtension.init).toHaveBeenCalledWith(app);
|
| 39 |
+
|
| 40 |
+
// Adding custom node defs should be passed the full list of nodes
|
| 41 |
+
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
|
| 42 |
+
expect(mockExtension.addCustomNodeDefs.mock.calls[0][1]).toStrictEqual(app);
|
| 43 |
+
const defs = mockExtension.addCustomNodeDefs.mock.calls[0][0];
|
| 44 |
+
expect(defs).toHaveProperty("KSampler");
|
| 45 |
+
expect(defs).toHaveProperty("LoadImage");
|
| 46 |
+
|
| 47 |
+
// Get custom widgets is called once and should return new widget types
|
| 48 |
+
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
|
| 49 |
+
expect(mockExtension.getCustomWidgets).toHaveBeenCalledWith(app);
|
| 50 |
+
|
| 51 |
+
// Before register node def will be called once per node type
|
| 52 |
+
const nodeNames = Object.keys(defs);
|
| 53 |
+
const nodeCount = nodeNames.length;
|
| 54 |
+
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount);
|
| 55 |
+
for (let i = 0; i < 10; i++) {
|
| 56 |
+
// It should be send the JS class and the original JSON definition
|
| 57 |
+
const nodeClass = mockExtension.beforeRegisterNodeDef.mock.calls[i][0];
|
| 58 |
+
const nodeDef = mockExtension.beforeRegisterNodeDef.mock.calls[i][1];
|
| 59 |
+
|
| 60 |
+
expect(nodeClass.name).toBe("ComfyNode");
|
| 61 |
+
expect(nodeClass.comfyClass).toBe(nodeNames[i]);
|
| 62 |
+
expect(nodeDef.name).toBe(nodeNames[i]);
|
| 63 |
+
expect(nodeDef).toHaveProperty("input");
|
| 64 |
+
expect(nodeDef).toHaveProperty("output");
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Register custom nodes is called once after registerNode defs to allow adding other frontend nodes
|
| 68 |
+
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
|
| 69 |
+
|
| 70 |
+
// Before configure graph will be called here as the default graph is being loaded
|
| 71 |
+
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(1);
|
| 72 |
+
// it gets sent the graph data that is going to be loaded
|
| 73 |
+
const graphData = mockExtension.beforeConfigureGraph.mock.calls[0][0];
|
| 74 |
+
|
| 75 |
+
// A node created is fired for each node constructor that is called
|
| 76 |
+
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length);
|
| 77 |
+
for (let i = 0; i < graphData.nodes.length; i++) {
|
| 78 |
+
expect(mockExtension.nodeCreated.mock.calls[i][0].type).toBe(graphData.nodes[i].type);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Each node then calls loadedGraphNode to allow them to be updated
|
| 82 |
+
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length);
|
| 83 |
+
for (let i = 0; i < graphData.nodes.length; i++) {
|
| 84 |
+
expect(mockExtension.loadedGraphNode.mock.calls[i][0].type).toBe(graphData.nodes[i].type);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// After configure is then called once all the setup is done
|
| 88 |
+
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(1);
|
| 89 |
+
|
| 90 |
+
expect(mockExtension.setup).toHaveBeenCalledTimes(1);
|
| 91 |
+
expect(mockExtension.setup).toHaveBeenCalledWith(app);
|
| 92 |
+
|
| 93 |
+
// Ensure hooks are called in the correct order
|
| 94 |
+
const callOrder = [
|
| 95 |
+
"init",
|
| 96 |
+
"addCustomNodeDefs",
|
| 97 |
+
"getCustomWidgets",
|
| 98 |
+
"beforeRegisterNodeDef",
|
| 99 |
+
"registerCustomNodes",
|
| 100 |
+
"beforeConfigureGraph",
|
| 101 |
+
"nodeCreated",
|
| 102 |
+
"loadedGraphNode",
|
| 103 |
+
"afterConfigureGraph",
|
| 104 |
+
"setup",
|
| 105 |
+
];
|
| 106 |
+
for (let i = 1; i < callOrder.length; i++) {
|
| 107 |
+
const fn1 = mockExtension[callOrder[i - 1]];
|
| 108 |
+
const fn2 = mockExtension[callOrder[i]];
|
| 109 |
+
expect(fn1.mock.invocationCallOrder[0]).toBeLessThan(fn2.mock.invocationCallOrder[0]);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
graph.clear();
|
| 113 |
+
|
| 114 |
+
// Ensure adding a new node calls the correct callback
|
| 115 |
+
ez.LoadImage();
|
| 116 |
+
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length);
|
| 117 |
+
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 1);
|
| 118 |
+
expect(mockExtension.nodeCreated.mock.lastCall[0].type).toBe("LoadImage");
|
| 119 |
+
|
| 120 |
+
// Reload the graph to ensure correct hooks are fired
|
| 121 |
+
await graph.reload();
|
| 122 |
+
|
| 123 |
+
// These hooks should not be fired again
|
| 124 |
+
expect(mockExtension.init).toHaveBeenCalledTimes(1);
|
| 125 |
+
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
|
| 126 |
+
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
|
| 127 |
+
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
|
| 128 |
+
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount);
|
| 129 |
+
expect(mockExtension.setup).toHaveBeenCalledTimes(1);
|
| 130 |
+
|
| 131 |
+
// These should be called again
|
| 132 |
+
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(2);
|
| 133 |
+
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 2);
|
| 134 |
+
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length + 1);
|
| 135 |
+
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(2);
|
| 136 |
+
}, 15000);
|
| 137 |
+
|
| 138 |
+
it("allows custom nodeDefs and widgets to be registered", async () => {
|
| 139 |
+
const widgetMock = jest.fn((node, inputName, inputData, app) => {
|
| 140 |
+
expect(node.constructor.comfyClass).toBe("TestNode");
|
| 141 |
+
expect(inputName).toBe("test_input");
|
| 142 |
+
expect(inputData[0]).toBe("CUSTOMWIDGET");
|
| 143 |
+
expect(inputData[1]?.hello).toBe("world");
|
| 144 |
+
expect(app).toStrictEqual(app);
|
| 145 |
+
|
| 146 |
+
return {
|
| 147 |
+
widget: node.addWidget("button", inputName, "hello", () => {}),
|
| 148 |
+
};
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
// Register our extension that adds a custom node + widget type
|
| 152 |
+
const mockExtension = {
|
| 153 |
+
name: "TestExtension",
|
| 154 |
+
addCustomNodeDefs: (nodeDefs) => {
|
| 155 |
+
nodeDefs["TestNode"] = {
|
| 156 |
+
output: [],
|
| 157 |
+
output_name: [],
|
| 158 |
+
output_is_list: [],
|
| 159 |
+
name: "TestNode",
|
| 160 |
+
display_name: "TestNode",
|
| 161 |
+
category: "Test",
|
| 162 |
+
input: {
|
| 163 |
+
required: {
|
| 164 |
+
test_input: ["CUSTOMWIDGET", { hello: "world" }],
|
| 165 |
+
},
|
| 166 |
+
},
|
| 167 |
+
};
|
| 168 |
+
},
|
| 169 |
+
getCustomWidgets: jest.fn(() => {
|
| 170 |
+
return {
|
| 171 |
+
CUSTOMWIDGET: widgetMock,
|
| 172 |
+
};
|
| 173 |
+
}),
|
| 174 |
+
};
|
| 175 |
+
|
| 176 |
+
const { graph, ez } = await start({
|
| 177 |
+
async preSetup(app) {
|
| 178 |
+
app.registerExtension(mockExtension);
|
| 179 |
+
},
|
| 180 |
+
});
|
| 181 |
+
|
| 182 |
+
expect(mockExtension.getCustomWidgets).toBeCalledTimes(1);
|
| 183 |
+
|
| 184 |
+
graph.clear();
|
| 185 |
+
expect(widgetMock).toBeCalledTimes(0);
|
| 186 |
+
const node = ez.TestNode();
|
| 187 |
+
expect(widgetMock).toBeCalledTimes(1);
|
| 188 |
+
|
| 189 |
+
// Ensure our custom widget is created
|
| 190 |
+
expect(node.inputs.length).toBe(0);
|
| 191 |
+
expect(node.widgets.length).toBe(1);
|
| 192 |
+
const w = node.widgets[0].widget;
|
| 193 |
+
expect(w.name).toBe("test_input");
|
| 194 |
+
expect(w.type).toBe("button");
|
| 195 |
+
});
|
| 196 |
+
});
|
ComfyUI/tests-ui/tests/groupNode.test.js
ADDED
|
@@ -0,0 +1,1005 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
/// <reference path="../node_modules/@types/jest/index.d.ts" />
|
| 3 |
+
|
| 4 |
+
const { start, createDefaultWorkflow, getNodeDef, checkBeforeAndAfterReload } = require("../utils");
|
| 5 |
+
const lg = require("../utils/litegraph");
|
| 6 |
+
|
| 7 |
+
describe("group node", () => {
|
| 8 |
+
beforeEach(() => {
|
| 9 |
+
lg.setup(global);
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
afterEach(() => {
|
| 13 |
+
lg.teardown(global);
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
*
|
| 18 |
+
* @param {*} app
|
| 19 |
+
* @param {*} graph
|
| 20 |
+
* @param {*} name
|
| 21 |
+
* @param {*} nodes
|
| 22 |
+
* @returns { Promise<InstanceType<import("../utils/ezgraph")["EzNode"]>> }
|
| 23 |
+
*/
|
| 24 |
+
async function convertToGroup(app, graph, name, nodes) {
|
| 25 |
+
// Select the nodes we are converting
|
| 26 |
+
for (const n of nodes) {
|
| 27 |
+
n.select(true);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
expect(Object.keys(app.canvas.selected_nodes).sort((a, b) => +a - +b)).toEqual(
|
| 31 |
+
nodes.map((n) => n.id + "").sort((a, b) => +a - +b)
|
| 32 |
+
);
|
| 33 |
+
|
| 34 |
+
global.prompt = jest.fn().mockImplementation(() => name);
|
| 35 |
+
const groupNode = await nodes[0].menu["Convert to Group Node"].call(false);
|
| 36 |
+
|
| 37 |
+
// Check group name was requested
|
| 38 |
+
expect(window.prompt).toHaveBeenCalled();
|
| 39 |
+
|
| 40 |
+
// Ensure old nodes are removed
|
| 41 |
+
for (const n of nodes) {
|
| 42 |
+
expect(n.isRemoved).toBeTruthy();
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
expect(groupNode.type).toEqual("workflow/" + name);
|
| 46 |
+
|
| 47 |
+
return graph.find(groupNode);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/**
|
| 51 |
+
* @param { Record<string, string | number> | number[] } idMap
|
| 52 |
+
* @param { Record<string, Record<string, unknown>> } valueMap
|
| 53 |
+
*/
|
| 54 |
+
function getOutput(idMap = {}, valueMap = {}) {
|
| 55 |
+
if (idMap instanceof Array) {
|
| 56 |
+
idMap = idMap.reduce((p, n) => {
|
| 57 |
+
p[n] = n + "";
|
| 58 |
+
return p;
|
| 59 |
+
}, {});
|
| 60 |
+
}
|
| 61 |
+
const expected = {
|
| 62 |
+
1: { inputs: { ckpt_name: "model1.safetensors", ...valueMap?.[1] }, class_type: "CheckpointLoaderSimple" },
|
| 63 |
+
2: { inputs: { text: "positive", clip: ["1", 1], ...valueMap?.[2] }, class_type: "CLIPTextEncode" },
|
| 64 |
+
3: { inputs: { text: "negative", clip: ["1", 1], ...valueMap?.[3] }, class_type: "CLIPTextEncode" },
|
| 65 |
+
4: { inputs: { width: 512, height: 512, batch_size: 1, ...valueMap?.[4] }, class_type: "EmptyLatentImage" },
|
| 66 |
+
5: {
|
| 67 |
+
inputs: {
|
| 68 |
+
seed: 0,
|
| 69 |
+
steps: 20,
|
| 70 |
+
cfg: 8,
|
| 71 |
+
sampler_name: "euler",
|
| 72 |
+
scheduler: "normal",
|
| 73 |
+
denoise: 1,
|
| 74 |
+
model: ["1", 0],
|
| 75 |
+
positive: ["2", 0],
|
| 76 |
+
negative: ["3", 0],
|
| 77 |
+
latent_image: ["4", 0],
|
| 78 |
+
...valueMap?.[5],
|
| 79 |
+
},
|
| 80 |
+
class_type: "KSampler",
|
| 81 |
+
},
|
| 82 |
+
6: { inputs: { samples: ["5", 0], vae: ["1", 2], ...valueMap?.[6] }, class_type: "VAEDecode" },
|
| 83 |
+
7: { inputs: { filename_prefix: "ComfyUI", images: ["6", 0], ...valueMap?.[7] }, class_type: "SaveImage" },
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
// Map old IDs to new at the top level
|
| 87 |
+
const mapped = {};
|
| 88 |
+
for (const oldId in idMap) {
|
| 89 |
+
mapped[idMap[oldId]] = expected[oldId];
|
| 90 |
+
delete expected[oldId];
|
| 91 |
+
}
|
| 92 |
+
Object.assign(mapped, expected);
|
| 93 |
+
|
| 94 |
+
// Map old IDs to new inside links
|
| 95 |
+
for (const k in mapped) {
|
| 96 |
+
for (const input in mapped[k].inputs) {
|
| 97 |
+
const v = mapped[k].inputs[input];
|
| 98 |
+
if (v instanceof Array) {
|
| 99 |
+
if (v[0] in idMap) {
|
| 100 |
+
v[0] = idMap[v[0]] + "";
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
return mapped;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
test("can be created from selected nodes", async () => {
|
| 110 |
+
const { ez, graph, app } = await start();
|
| 111 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 112 |
+
const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg, nodes.empty]);
|
| 113 |
+
|
| 114 |
+
// Ensure links are now to the group node
|
| 115 |
+
expect(group.inputs).toHaveLength(2);
|
| 116 |
+
expect(group.outputs).toHaveLength(3);
|
| 117 |
+
|
| 118 |
+
expect(group.inputs.map((i) => i.input.name)).toEqual(["clip", "CLIPTextEncode clip"]);
|
| 119 |
+
expect(group.outputs.map((i) => i.output.name)).toEqual(["LATENT", "CONDITIONING", "CLIPTextEncode CONDITIONING"]);
|
| 120 |
+
|
| 121 |
+
// ckpt clip to both clip inputs on the group
|
| 122 |
+
expect(nodes.ckpt.outputs.CLIP.connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
|
| 123 |
+
[group.id, 0],
|
| 124 |
+
[group.id, 1],
|
| 125 |
+
]);
|
| 126 |
+
|
| 127 |
+
// group conditioning to sampler
|
| 128 |
+
expect(group.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
|
| 129 |
+
[nodes.sampler.id, 1],
|
| 130 |
+
]);
|
| 131 |
+
// group conditioning 2 to sampler
|
| 132 |
+
expect(
|
| 133 |
+
group.outputs["CLIPTextEncode CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])
|
| 134 |
+
).toEqual([[nodes.sampler.id, 2]]);
|
| 135 |
+
// group latent to sampler
|
| 136 |
+
expect(group.outputs["LATENT"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
|
| 137 |
+
[nodes.sampler.id, 3],
|
| 138 |
+
]);
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
test("maintains all output links on conversion", async () => {
|
| 142 |
+
const { ez, graph, app } = await start();
|
| 143 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 144 |
+
const save2 = ez.SaveImage(...nodes.decode.outputs);
|
| 145 |
+
const save3 = ez.SaveImage(...nodes.decode.outputs);
|
| 146 |
+
// Ensure an output with multiple links maintains them on convert to group
|
| 147 |
+
const group = await convertToGroup(app, graph, "test", [nodes.sampler, nodes.decode]);
|
| 148 |
+
expect(group.outputs[0].connections.length).toBe(3);
|
| 149 |
+
expect(group.outputs[0].connections[0].targetNode.id).toBe(nodes.save.id);
|
| 150 |
+
expect(group.outputs[0].connections[1].targetNode.id).toBe(save2.id);
|
| 151 |
+
expect(group.outputs[0].connections[2].targetNode.id).toBe(save3.id);
|
| 152 |
+
|
| 153 |
+
// and they're still linked when converting back to nodes
|
| 154 |
+
const newNodes = group.menu["Convert to nodes"].call();
|
| 155 |
+
const decode = graph.find(newNodes.find((n) => n.type === "VAEDecode"));
|
| 156 |
+
expect(decode.outputs[0].connections.length).toBe(3);
|
| 157 |
+
expect(decode.outputs[0].connections[0].targetNode.id).toBe(nodes.save.id);
|
| 158 |
+
expect(decode.outputs[0].connections[1].targetNode.id).toBe(save2.id);
|
| 159 |
+
expect(decode.outputs[0].connections[2].targetNode.id).toBe(save3.id);
|
| 160 |
+
});
|
| 161 |
+
test("can be be converted back to nodes", async () => {
|
| 162 |
+
const { ez, graph, app } = await start();
|
| 163 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 164 |
+
const toConvert = [nodes.pos, nodes.neg, nodes.empty, nodes.sampler];
|
| 165 |
+
const group = await convertToGroup(app, graph, "test", toConvert);
|
| 166 |
+
|
| 167 |
+
// Edit some values to ensure they are set back onto the converted nodes
|
| 168 |
+
expect(group.widgets["text"].value).toBe("positive");
|
| 169 |
+
group.widgets["text"].value = "pos";
|
| 170 |
+
expect(group.widgets["CLIPTextEncode text"].value).toBe("negative");
|
| 171 |
+
group.widgets["CLIPTextEncode text"].value = "neg";
|
| 172 |
+
expect(group.widgets["width"].value).toBe(512);
|
| 173 |
+
group.widgets["width"].value = 1024;
|
| 174 |
+
expect(group.widgets["sampler_name"].value).toBe("euler");
|
| 175 |
+
group.widgets["sampler_name"].value = "ddim";
|
| 176 |
+
expect(group.widgets["control_after_generate"].value).toBe("randomize");
|
| 177 |
+
group.widgets["control_after_generate"].value = "fixed";
|
| 178 |
+
|
| 179 |
+
/** @type { Array<any> } */
|
| 180 |
+
group.menu["Convert to nodes"].call();
|
| 181 |
+
|
| 182 |
+
// ensure widget values are set
|
| 183 |
+
const pos = graph.find(nodes.pos.id);
|
| 184 |
+
expect(pos.node.type).toBe("CLIPTextEncode");
|
| 185 |
+
expect(pos.widgets["text"].value).toBe("pos");
|
| 186 |
+
const neg = graph.find(nodes.neg.id);
|
| 187 |
+
expect(neg.node.type).toBe("CLIPTextEncode");
|
| 188 |
+
expect(neg.widgets["text"].value).toBe("neg");
|
| 189 |
+
const empty = graph.find(nodes.empty.id);
|
| 190 |
+
expect(empty.node.type).toBe("EmptyLatentImage");
|
| 191 |
+
expect(empty.widgets["width"].value).toBe(1024);
|
| 192 |
+
const sampler = graph.find(nodes.sampler.id);
|
| 193 |
+
expect(sampler.node.type).toBe("KSampler");
|
| 194 |
+
expect(sampler.widgets["sampler_name"].value).toBe("ddim");
|
| 195 |
+
expect(sampler.widgets["control_after_generate"].value).toBe("fixed");
|
| 196 |
+
|
| 197 |
+
// validate links
|
| 198 |
+
expect(nodes.ckpt.outputs.CLIP.connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
|
| 199 |
+
[pos.id, 0],
|
| 200 |
+
[neg.id, 0],
|
| 201 |
+
]);
|
| 202 |
+
|
| 203 |
+
expect(pos.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
|
| 204 |
+
[nodes.sampler.id, 1],
|
| 205 |
+
]);
|
| 206 |
+
|
| 207 |
+
expect(neg.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
|
| 208 |
+
[nodes.sampler.id, 2],
|
| 209 |
+
]);
|
| 210 |
+
|
| 211 |
+
expect(empty.outputs["LATENT"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
|
| 212 |
+
[nodes.sampler.id, 3],
|
| 213 |
+
]);
|
| 214 |
+
});
|
| 215 |
+
test("it can embed reroutes as inputs", async () => {
|
| 216 |
+
const { ez, graph, app } = await start();
|
| 217 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 218 |
+
|
| 219 |
+
// Add and connect a reroute to the clip text encodes
|
| 220 |
+
const reroute = ez.Reroute();
|
| 221 |
+
nodes.ckpt.outputs.CLIP.connectTo(reroute.inputs[0]);
|
| 222 |
+
reroute.outputs[0].connectTo(nodes.pos.inputs[0]);
|
| 223 |
+
reroute.outputs[0].connectTo(nodes.neg.inputs[0]);
|
| 224 |
+
|
| 225 |
+
// Convert to group and ensure we only have 1 input of the correct type
|
| 226 |
+
const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg, nodes.empty, reroute]);
|
| 227 |
+
expect(group.inputs).toHaveLength(1);
|
| 228 |
+
expect(group.inputs[0].input.type).toEqual("CLIP");
|
| 229 |
+
|
| 230 |
+
expect((await graph.toPrompt()).output).toEqual(getOutput());
|
| 231 |
+
});
|
| 232 |
+
test("it can embed reroutes as outputs", async () => {
|
| 233 |
+
const { ez, graph, app } = await start();
|
| 234 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 235 |
+
|
| 236 |
+
// Add a reroute with no output so we output IMAGE even though its used internally
|
| 237 |
+
const reroute = ez.Reroute();
|
| 238 |
+
nodes.decode.outputs.IMAGE.connectTo(reroute.inputs[0]);
|
| 239 |
+
|
| 240 |
+
// Convert to group and ensure there is an IMAGE output
|
| 241 |
+
const group = await convertToGroup(app, graph, "test", [nodes.decode, nodes.save, reroute]);
|
| 242 |
+
expect(group.outputs).toHaveLength(1);
|
| 243 |
+
expect(group.outputs[0].output.type).toEqual("IMAGE");
|
| 244 |
+
expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.decode.id, nodes.save.id]));
|
| 245 |
+
});
|
| 246 |
+
test("it can embed reroutes as pipes", async () => {
|
| 247 |
+
const { ez, graph, app } = await start();
|
| 248 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 249 |
+
|
| 250 |
+
// Use reroutes as a pipe
|
| 251 |
+
const rerouteModel = ez.Reroute();
|
| 252 |
+
const rerouteClip = ez.Reroute();
|
| 253 |
+
const rerouteVae = ez.Reroute();
|
| 254 |
+
nodes.ckpt.outputs.MODEL.connectTo(rerouteModel.inputs[0]);
|
| 255 |
+
nodes.ckpt.outputs.CLIP.connectTo(rerouteClip.inputs[0]);
|
| 256 |
+
nodes.ckpt.outputs.VAE.connectTo(rerouteVae.inputs[0]);
|
| 257 |
+
|
| 258 |
+
const group = await convertToGroup(app, graph, "test", [rerouteModel, rerouteClip, rerouteVae]);
|
| 259 |
+
|
| 260 |
+
expect(group.outputs).toHaveLength(3);
|
| 261 |
+
expect(group.outputs.map((o) => o.output.type)).toEqual(["MODEL", "CLIP", "VAE"]);
|
| 262 |
+
|
| 263 |
+
expect(group.outputs).toHaveLength(3);
|
| 264 |
+
expect(group.outputs.map((o) => o.output.type)).toEqual(["MODEL", "CLIP", "VAE"]);
|
| 265 |
+
|
| 266 |
+
group.outputs[0].connectTo(nodes.sampler.inputs.model);
|
| 267 |
+
group.outputs[1].connectTo(nodes.pos.inputs.clip);
|
| 268 |
+
group.outputs[1].connectTo(nodes.neg.inputs.clip);
|
| 269 |
+
});
|
| 270 |
+
test("can handle reroutes used internally", async () => {
|
| 271 |
+
const { ez, graph, app } = await start();
|
| 272 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 273 |
+
|
| 274 |
+
let reroutes = [];
|
| 275 |
+
let prevNode = nodes.ckpt;
|
| 276 |
+
for (let i = 0; i < 5; i++) {
|
| 277 |
+
const reroute = ez.Reroute();
|
| 278 |
+
prevNode.outputs[0].connectTo(reroute.inputs[0]);
|
| 279 |
+
prevNode = reroute;
|
| 280 |
+
reroutes.push(reroute);
|
| 281 |
+
}
|
| 282 |
+
prevNode.outputs[0].connectTo(nodes.sampler.inputs.model);
|
| 283 |
+
|
| 284 |
+
const group = await convertToGroup(app, graph, "test", [...reroutes, ...Object.values(nodes)]);
|
| 285 |
+
expect((await graph.toPrompt()).output).toEqual(getOutput());
|
| 286 |
+
|
| 287 |
+
group.menu["Convert to nodes"].call();
|
| 288 |
+
expect((await graph.toPrompt()).output).toEqual(getOutput());
|
| 289 |
+
});
|
| 290 |
+
test("creates with widget values from inner nodes", async () => {
|
| 291 |
+
const { ez, graph, app } = await start();
|
| 292 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 293 |
+
|
| 294 |
+
nodes.ckpt.widgets.ckpt_name.value = "model2.ckpt";
|
| 295 |
+
nodes.pos.widgets.text.value = "hello";
|
| 296 |
+
nodes.neg.widgets.text.value = "world";
|
| 297 |
+
nodes.empty.widgets.width.value = 256;
|
| 298 |
+
nodes.empty.widgets.height.value = 1024;
|
| 299 |
+
nodes.sampler.widgets.seed.value = 1;
|
| 300 |
+
nodes.sampler.widgets.control_after_generate.value = "increment";
|
| 301 |
+
nodes.sampler.widgets.steps.value = 8;
|
| 302 |
+
nodes.sampler.widgets.cfg.value = 4.5;
|
| 303 |
+
nodes.sampler.widgets.sampler_name.value = "uni_pc";
|
| 304 |
+
nodes.sampler.widgets.scheduler.value = "karras";
|
| 305 |
+
nodes.sampler.widgets.denoise.value = 0.9;
|
| 306 |
+
|
| 307 |
+
const group = await convertToGroup(app, graph, "test", [
|
| 308 |
+
nodes.ckpt,
|
| 309 |
+
nodes.pos,
|
| 310 |
+
nodes.neg,
|
| 311 |
+
nodes.empty,
|
| 312 |
+
nodes.sampler,
|
| 313 |
+
]);
|
| 314 |
+
|
| 315 |
+
expect(group.widgets["ckpt_name"].value).toEqual("model2.ckpt");
|
| 316 |
+
expect(group.widgets["text"].value).toEqual("hello");
|
| 317 |
+
expect(group.widgets["CLIPTextEncode text"].value).toEqual("world");
|
| 318 |
+
expect(group.widgets["width"].value).toEqual(256);
|
| 319 |
+
expect(group.widgets["height"].value).toEqual(1024);
|
| 320 |
+
expect(group.widgets["seed"].value).toEqual(1);
|
| 321 |
+
expect(group.widgets["control_after_generate"].value).toEqual("increment");
|
| 322 |
+
expect(group.widgets["steps"].value).toEqual(8);
|
| 323 |
+
expect(group.widgets["cfg"].value).toEqual(4.5);
|
| 324 |
+
expect(group.widgets["sampler_name"].value).toEqual("uni_pc");
|
| 325 |
+
expect(group.widgets["scheduler"].value).toEqual("karras");
|
| 326 |
+
expect(group.widgets["denoise"].value).toEqual(0.9);
|
| 327 |
+
|
| 328 |
+
expect((await graph.toPrompt()).output).toEqual(
|
| 329 |
+
getOutput([nodes.ckpt.id, nodes.pos.id, nodes.neg.id, nodes.empty.id, nodes.sampler.id], {
|
| 330 |
+
[nodes.ckpt.id]: { ckpt_name: "model2.ckpt" },
|
| 331 |
+
[nodes.pos.id]: { text: "hello" },
|
| 332 |
+
[nodes.neg.id]: { text: "world" },
|
| 333 |
+
[nodes.empty.id]: { width: 256, height: 1024 },
|
| 334 |
+
[nodes.sampler.id]: {
|
| 335 |
+
seed: 1,
|
| 336 |
+
steps: 8,
|
| 337 |
+
cfg: 4.5,
|
| 338 |
+
sampler_name: "uni_pc",
|
| 339 |
+
scheduler: "karras",
|
| 340 |
+
denoise: 0.9,
|
| 341 |
+
},
|
| 342 |
+
})
|
| 343 |
+
);
|
| 344 |
+
});
|
| 345 |
+
test("group inputs can be reroutes", async () => {
|
| 346 |
+
const { ez, graph, app } = await start();
|
| 347 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 348 |
+
const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
|
| 349 |
+
|
| 350 |
+
const reroute = ez.Reroute();
|
| 351 |
+
nodes.ckpt.outputs.CLIP.connectTo(reroute.inputs[0]);
|
| 352 |
+
|
| 353 |
+
reroute.outputs[0].connectTo(group.inputs[0]);
|
| 354 |
+
reroute.outputs[0].connectTo(group.inputs[1]);
|
| 355 |
+
|
| 356 |
+
expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.pos.id, nodes.neg.id]));
|
| 357 |
+
});
|
| 358 |
+
test("group outputs can be reroutes", async () => {
|
| 359 |
+
const { ez, graph, app } = await start();
|
| 360 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 361 |
+
const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
|
| 362 |
+
|
| 363 |
+
const reroute1 = ez.Reroute();
|
| 364 |
+
const reroute2 = ez.Reroute();
|
| 365 |
+
group.outputs[0].connectTo(reroute1.inputs[0]);
|
| 366 |
+
group.outputs[1].connectTo(reroute2.inputs[0]);
|
| 367 |
+
|
| 368 |
+
reroute1.outputs[0].connectTo(nodes.sampler.inputs.positive);
|
| 369 |
+
reroute2.outputs[0].connectTo(nodes.sampler.inputs.negative);
|
| 370 |
+
|
| 371 |
+
expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.pos.id, nodes.neg.id]));
|
| 372 |
+
});
|
| 373 |
+
test("groups can connect to each other", async () => {
|
| 374 |
+
const { ez, graph, app } = await start();
|
| 375 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 376 |
+
const group1 = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
|
| 377 |
+
const group2 = await convertToGroup(app, graph, "test2", [nodes.empty, nodes.sampler]);
|
| 378 |
+
|
| 379 |
+
group1.outputs[0].connectTo(group2.inputs["positive"]);
|
| 380 |
+
group1.outputs[1].connectTo(group2.inputs["negative"]);
|
| 381 |
+
|
| 382 |
+
expect((await graph.toPrompt()).output).toEqual(
|
| 383 |
+
getOutput([nodes.pos.id, nodes.neg.id, nodes.empty.id, nodes.sampler.id])
|
| 384 |
+
);
|
| 385 |
+
});
|
| 386 |
+
test("groups can connect to each other via internal reroutes", async () => {
|
| 387 |
+
const { ez, graph, app } = await start();
|
| 388 |
+
|
| 389 |
+
const latent = ez.EmptyLatentImage();
|
| 390 |
+
const vae = ez.VAELoader();
|
| 391 |
+
const latentReroute = ez.Reroute();
|
| 392 |
+
const vaeReroute = ez.Reroute();
|
| 393 |
+
|
| 394 |
+
latent.outputs[0].connectTo(latentReroute.inputs[0]);
|
| 395 |
+
vae.outputs[0].connectTo(vaeReroute.inputs[0]);
|
| 396 |
+
|
| 397 |
+
const group1 = await convertToGroup(app, graph, "test", [latentReroute, vaeReroute]);
|
| 398 |
+
group1.menu.Clone.call();
|
| 399 |
+
expect(app.graph._nodes).toHaveLength(4);
|
| 400 |
+
const group2 = graph.find(app.graph._nodes[3]);
|
| 401 |
+
expect(group2.node.type).toEqual("workflow/test");
|
| 402 |
+
expect(group2.id).not.toEqual(group1.id);
|
| 403 |
+
|
| 404 |
+
group1.outputs.VAE.connectTo(group2.inputs.VAE);
|
| 405 |
+
group1.outputs.LATENT.connectTo(group2.inputs.LATENT);
|
| 406 |
+
|
| 407 |
+
const decode = ez.VAEDecode(group2.outputs.LATENT, group2.outputs.VAE);
|
| 408 |
+
const preview = ez.PreviewImage(decode.outputs[0]);
|
| 409 |
+
|
| 410 |
+
const output = {
|
| 411 |
+
[latent.id]: { inputs: { width: 512, height: 512, batch_size: 1 }, class_type: "EmptyLatentImage" },
|
| 412 |
+
[vae.id]: { inputs: { vae_name: "vae1.safetensors" }, class_type: "VAELoader" },
|
| 413 |
+
[decode.id]: { inputs: { samples: [latent.id + "", 0], vae: [vae.id + "", 0] }, class_type: "VAEDecode" },
|
| 414 |
+
[preview.id]: { inputs: { images: [decode.id + "", 0] }, class_type: "PreviewImage" },
|
| 415 |
+
};
|
| 416 |
+
expect((await graph.toPrompt()).output).toEqual(output);
|
| 417 |
+
|
| 418 |
+
// Ensure missing connections dont cause errors
|
| 419 |
+
group2.inputs.VAE.disconnect();
|
| 420 |
+
delete output[decode.id].inputs.vae;
|
| 421 |
+
expect((await graph.toPrompt()).output).toEqual(output);
|
| 422 |
+
});
|
| 423 |
+
test("displays generated image on group node", async () => {
|
| 424 |
+
const { ez, graph, app } = await start();
|
| 425 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 426 |
+
let group = await convertToGroup(app, graph, "test", [
|
| 427 |
+
nodes.pos,
|
| 428 |
+
nodes.neg,
|
| 429 |
+
nodes.empty,
|
| 430 |
+
nodes.sampler,
|
| 431 |
+
nodes.decode,
|
| 432 |
+
nodes.save,
|
| 433 |
+
]);
|
| 434 |
+
|
| 435 |
+
const { api } = require("../../web/scripts/api");
|
| 436 |
+
|
| 437 |
+
api.dispatchEvent(new CustomEvent("execution_start", {}));
|
| 438 |
+
api.dispatchEvent(new CustomEvent("executing", { detail: `${nodes.save.id}` }));
|
| 439 |
+
// Event should be forwarded to group node id
|
| 440 |
+
expect(+app.runningNodeId).toEqual(group.id);
|
| 441 |
+
expect(group.node["imgs"]).toBeFalsy();
|
| 442 |
+
api.dispatchEvent(
|
| 443 |
+
new CustomEvent("executed", {
|
| 444 |
+
detail: {
|
| 445 |
+
node: `${nodes.save.id}`,
|
| 446 |
+
output: {
|
| 447 |
+
images: [
|
| 448 |
+
{
|
| 449 |
+
filename: "test.png",
|
| 450 |
+
type: "output",
|
| 451 |
+
},
|
| 452 |
+
],
|
| 453 |
+
},
|
| 454 |
+
},
|
| 455 |
+
})
|
| 456 |
+
);
|
| 457 |
+
|
| 458 |
+
// Trigger paint
|
| 459 |
+
group.node.onDrawBackground?.(app.canvas.ctx, app.canvas.canvas);
|
| 460 |
+
|
| 461 |
+
expect(group.node["images"]).toEqual([
|
| 462 |
+
{
|
| 463 |
+
filename: "test.png",
|
| 464 |
+
type: "output",
|
| 465 |
+
},
|
| 466 |
+
]);
|
| 467 |
+
|
| 468 |
+
// Reload
|
| 469 |
+
const workflow = JSON.stringify((await graph.toPrompt()).workflow);
|
| 470 |
+
await app.loadGraphData(JSON.parse(workflow));
|
| 471 |
+
group = graph.find(group);
|
| 472 |
+
|
| 473 |
+
// Trigger inner nodes to get created
|
| 474 |
+
group.node["getInnerNodes"]();
|
| 475 |
+
|
| 476 |
+
// Check it works for internal node ids
|
| 477 |
+
api.dispatchEvent(new CustomEvent("execution_start", {}));
|
| 478 |
+
api.dispatchEvent(new CustomEvent("executing", { detail: `${group.id}:5` }));
|
| 479 |
+
// Event should be forwarded to group node id
|
| 480 |
+
expect(+app.runningNodeId).toEqual(group.id);
|
| 481 |
+
expect(group.node["imgs"]).toBeFalsy();
|
| 482 |
+
api.dispatchEvent(
|
| 483 |
+
new CustomEvent("executed", {
|
| 484 |
+
detail: {
|
| 485 |
+
node: `${group.id}:5`,
|
| 486 |
+
output: {
|
| 487 |
+
images: [
|
| 488 |
+
{
|
| 489 |
+
filename: "test2.png",
|
| 490 |
+
type: "output",
|
| 491 |
+
},
|
| 492 |
+
],
|
| 493 |
+
},
|
| 494 |
+
},
|
| 495 |
+
})
|
| 496 |
+
);
|
| 497 |
+
|
| 498 |
+
// Trigger paint
|
| 499 |
+
group.node.onDrawBackground?.(app.canvas.ctx, app.canvas.canvas);
|
| 500 |
+
|
| 501 |
+
expect(group.node["images"]).toEqual([
|
| 502 |
+
{
|
| 503 |
+
filename: "test2.png",
|
| 504 |
+
type: "output",
|
| 505 |
+
},
|
| 506 |
+
]);
|
| 507 |
+
});
|
| 508 |
+
test("allows widgets to be converted to inputs", async () => {
|
| 509 |
+
const { ez, graph, app } = await start();
|
| 510 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 511 |
+
const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
|
| 512 |
+
group.widgets[0].convertToInput();
|
| 513 |
+
|
| 514 |
+
const primitive = ez.PrimitiveNode();
|
| 515 |
+
primitive.outputs[0].connectTo(group.inputs["text"]);
|
| 516 |
+
primitive.widgets[0].value = "hello";
|
| 517 |
+
|
| 518 |
+
expect((await graph.toPrompt()).output).toEqual(
|
| 519 |
+
getOutput([nodes.pos.id, nodes.neg.id], {
|
| 520 |
+
[nodes.pos.id]: { text: "hello" },
|
| 521 |
+
})
|
| 522 |
+
);
|
| 523 |
+
});
|
| 524 |
+
test("can be copied", async () => {
|
| 525 |
+
const { ez, graph, app } = await start();
|
| 526 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 527 |
+
|
| 528 |
+
const group1 = await convertToGroup(app, graph, "test", [
|
| 529 |
+
nodes.pos,
|
| 530 |
+
nodes.neg,
|
| 531 |
+
nodes.empty,
|
| 532 |
+
nodes.sampler,
|
| 533 |
+
nodes.decode,
|
| 534 |
+
nodes.save,
|
| 535 |
+
]);
|
| 536 |
+
|
| 537 |
+
group1.widgets["text"].value = "hello";
|
| 538 |
+
group1.widgets["width"].value = 256;
|
| 539 |
+
group1.widgets["seed"].value = 1;
|
| 540 |
+
|
| 541 |
+
// Clone the node
|
| 542 |
+
group1.menu.Clone.call();
|
| 543 |
+
expect(app.graph._nodes).toHaveLength(3);
|
| 544 |
+
const group2 = graph.find(app.graph._nodes[2]);
|
| 545 |
+
expect(group2.node.type).toEqual("workflow/test");
|
| 546 |
+
expect(group2.id).not.toEqual(group1.id);
|
| 547 |
+
|
| 548 |
+
// Reconnect ckpt
|
| 549 |
+
nodes.ckpt.outputs.MODEL.connectTo(group2.inputs["model"]);
|
| 550 |
+
nodes.ckpt.outputs.CLIP.connectTo(group2.inputs["clip"]);
|
| 551 |
+
nodes.ckpt.outputs.CLIP.connectTo(group2.inputs["CLIPTextEncode clip"]);
|
| 552 |
+
nodes.ckpt.outputs.VAE.connectTo(group2.inputs["vae"]);
|
| 553 |
+
|
| 554 |
+
group2.widgets["text"].value = "world";
|
| 555 |
+
group2.widgets["width"].value = 1024;
|
| 556 |
+
group2.widgets["seed"].value = 100;
|
| 557 |
+
|
| 558 |
+
let i = 0;
|
| 559 |
+
expect((await graph.toPrompt()).output).toEqual({
|
| 560 |
+
...getOutput([nodes.empty.id, nodes.pos.id, nodes.neg.id, nodes.sampler.id, nodes.decode.id, nodes.save.id], {
|
| 561 |
+
[nodes.empty.id]: { width: 256 },
|
| 562 |
+
[nodes.pos.id]: { text: "hello" },
|
| 563 |
+
[nodes.sampler.id]: { seed: 1 },
|
| 564 |
+
}),
|
| 565 |
+
...getOutput(
|
| 566 |
+
{
|
| 567 |
+
[nodes.empty.id]: `${group2.id}:${i++}`,
|
| 568 |
+
[nodes.pos.id]: `${group2.id}:${i++}`,
|
| 569 |
+
[nodes.neg.id]: `${group2.id}:${i++}`,
|
| 570 |
+
[nodes.sampler.id]: `${group2.id}:${i++}`,
|
| 571 |
+
[nodes.decode.id]: `${group2.id}:${i++}`,
|
| 572 |
+
[nodes.save.id]: `${group2.id}:${i++}`,
|
| 573 |
+
},
|
| 574 |
+
{
|
| 575 |
+
[nodes.empty.id]: { width: 1024 },
|
| 576 |
+
[nodes.pos.id]: { text: "world" },
|
| 577 |
+
[nodes.sampler.id]: { seed: 100 },
|
| 578 |
+
}
|
| 579 |
+
),
|
| 580 |
+
});
|
| 581 |
+
|
| 582 |
+
graph.arrange();
|
| 583 |
+
});
|
| 584 |
+
test("is embedded in workflow", async () => {
|
| 585 |
+
let { ez, graph, app } = await start();
|
| 586 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 587 |
+
let group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
|
| 588 |
+
const workflow = JSON.stringify((await graph.toPrompt()).workflow);
|
| 589 |
+
|
| 590 |
+
// Clear the environment
|
| 591 |
+
({ ez, graph, app } = await start({
|
| 592 |
+
resetEnv: true,
|
| 593 |
+
}));
|
| 594 |
+
// Ensure the node isnt registered
|
| 595 |
+
expect(() => ez["workflow/test"]).toThrow();
|
| 596 |
+
|
| 597 |
+
// Reload the workflow
|
| 598 |
+
await app.loadGraphData(JSON.parse(workflow));
|
| 599 |
+
|
| 600 |
+
// Ensure the node is found
|
| 601 |
+
group = graph.find(group);
|
| 602 |
+
|
| 603 |
+
// Generate prompt and ensure it is as expected
|
| 604 |
+
expect((await graph.toPrompt()).output).toEqual(
|
| 605 |
+
getOutput({
|
| 606 |
+
[nodes.pos.id]: `${group.id}:0`,
|
| 607 |
+
[nodes.neg.id]: `${group.id}:1`,
|
| 608 |
+
})
|
| 609 |
+
);
|
| 610 |
+
});
|
| 611 |
+
test("shows missing node error on missing internal node when loading graph data", async () => {
|
| 612 |
+
const { graph } = await start();
|
| 613 |
+
|
| 614 |
+
const dialogShow = jest.spyOn(graph.app.ui.dialog, "show");
|
| 615 |
+
await graph.app.loadGraphData({
|
| 616 |
+
last_node_id: 3,
|
| 617 |
+
last_link_id: 1,
|
| 618 |
+
nodes: [
|
| 619 |
+
{
|
| 620 |
+
id: 3,
|
| 621 |
+
type: "workflow/testerror",
|
| 622 |
+
},
|
| 623 |
+
],
|
| 624 |
+
links: [],
|
| 625 |
+
groups: [],
|
| 626 |
+
config: {},
|
| 627 |
+
extra: {
|
| 628 |
+
groupNodes: {
|
| 629 |
+
testerror: {
|
| 630 |
+
nodes: [
|
| 631 |
+
{
|
| 632 |
+
type: "NotKSampler",
|
| 633 |
+
},
|
| 634 |
+
{
|
| 635 |
+
type: "NotVAEDecode",
|
| 636 |
+
},
|
| 637 |
+
],
|
| 638 |
+
},
|
| 639 |
+
},
|
| 640 |
+
},
|
| 641 |
+
});
|
| 642 |
+
|
| 643 |
+
expect(dialogShow).toBeCalledTimes(1);
|
| 644 |
+
const call = dialogShow.mock.calls[0][0].innerHTML;
|
| 645 |
+
expect(call).toContain("the following node types were not found");
|
| 646 |
+
expect(call).toContain("NotKSampler");
|
| 647 |
+
expect(call).toContain("NotVAEDecode");
|
| 648 |
+
expect(call).toContain("workflow/testerror");
|
| 649 |
+
});
|
| 650 |
+
test("maintains widget inputs on conversion back to nodes", async () => {
|
| 651 |
+
const { ez, graph, app } = await start();
|
| 652 |
+
let pos = ez.CLIPTextEncode({ text: "positive" });
|
| 653 |
+
pos.node.title = "Positive";
|
| 654 |
+
let neg = ez.CLIPTextEncode({ text: "negative" });
|
| 655 |
+
neg.node.title = "Negative";
|
| 656 |
+
pos.widgets.text.convertToInput();
|
| 657 |
+
neg.widgets.text.convertToInput();
|
| 658 |
+
|
| 659 |
+
let primitive = ez.PrimitiveNode();
|
| 660 |
+
primitive.outputs[0].connectTo(pos.inputs.text);
|
| 661 |
+
primitive.outputs[0].connectTo(neg.inputs.text);
|
| 662 |
+
|
| 663 |
+
const group = await convertToGroup(app, graph, "test", [pos, neg, primitive]);
|
| 664 |
+
// This will use a primitive widget named 'value'
|
| 665 |
+
expect(group.widgets.length).toBe(1);
|
| 666 |
+
expect(group.widgets["value"].value).toBe("positive");
|
| 667 |
+
|
| 668 |
+
const newNodes = group.menu["Convert to nodes"].call();
|
| 669 |
+
pos = graph.find(newNodes.find((n) => n.title === "Positive"));
|
| 670 |
+
neg = graph.find(newNodes.find((n) => n.title === "Negative"));
|
| 671 |
+
primitive = graph.find(newNodes.find((n) => n.type === "PrimitiveNode"));
|
| 672 |
+
|
| 673 |
+
expect(pos.inputs).toHaveLength(2);
|
| 674 |
+
expect(neg.inputs).toHaveLength(2);
|
| 675 |
+
expect(primitive.outputs[0].connections).toHaveLength(2);
|
| 676 |
+
|
| 677 |
+
expect((await graph.toPrompt()).output).toEqual({
|
| 678 |
+
1: { inputs: { text: "positive" }, class_type: "CLIPTextEncode" },
|
| 679 |
+
2: { inputs: { text: "positive" }, class_type: "CLIPTextEncode" },
|
| 680 |
+
});
|
| 681 |
+
});
|
| 682 |
+
test("correctly handles widget inputs", async () => {
|
| 683 |
+
const { ez, graph, app } = await start();
|
| 684 |
+
const upscaleMethods = (await getNodeDef("ImageScaleBy")).input.required["upscale_method"][0];
|
| 685 |
+
|
| 686 |
+
const image = ez.LoadImage();
|
| 687 |
+
const scale1 = ez.ImageScaleBy(image.outputs[0]);
|
| 688 |
+
const scale2 = ez.ImageScaleBy(image.outputs[0]);
|
| 689 |
+
const preview1 = ez.PreviewImage(scale1.outputs[0]);
|
| 690 |
+
const preview2 = ez.PreviewImage(scale2.outputs[0]);
|
| 691 |
+
scale1.widgets.upscale_method.value = upscaleMethods[1];
|
| 692 |
+
scale1.widgets.upscale_method.convertToInput();
|
| 693 |
+
|
| 694 |
+
const group = await convertToGroup(app, graph, "test", [scale1, scale2]);
|
| 695 |
+
expect(group.inputs.length).toBe(3);
|
| 696 |
+
expect(group.inputs[0].input.type).toBe("IMAGE");
|
| 697 |
+
expect(group.inputs[1].input.type).toBe("IMAGE");
|
| 698 |
+
expect(group.inputs[2].input.type).toBe("COMBO");
|
| 699 |
+
|
| 700 |
+
// Ensure links are maintained
|
| 701 |
+
expect(group.inputs[0].connection?.originNode?.id).toBe(image.id);
|
| 702 |
+
expect(group.inputs[1].connection?.originNode?.id).toBe(image.id);
|
| 703 |
+
expect(group.inputs[2].connection).toBeFalsy();
|
| 704 |
+
|
| 705 |
+
// Ensure primitive gets correct type
|
| 706 |
+
const primitive = ez.PrimitiveNode();
|
| 707 |
+
primitive.outputs[0].connectTo(group.inputs[2]);
|
| 708 |
+
expect(primitive.widgets.value.widget.options.values).toBe(upscaleMethods);
|
| 709 |
+
expect(primitive.widgets.value.value).toBe(upscaleMethods[1]); // Ensure value is copied
|
| 710 |
+
primitive.widgets.value.value = upscaleMethods[1];
|
| 711 |
+
|
| 712 |
+
await checkBeforeAndAfterReload(graph, async (r) => {
|
| 713 |
+
const scale1id = r ? `${group.id}:0` : scale1.id;
|
| 714 |
+
const scale2id = r ? `${group.id}:1` : scale2.id;
|
| 715 |
+
// Ensure widget value is applied to prompt
|
| 716 |
+
expect((await graph.toPrompt()).output).toStrictEqual({
|
| 717 |
+
[image.id]: { inputs: { image: "example.png", upload: "image" }, class_type: "LoadImage" },
|
| 718 |
+
[scale1id]: {
|
| 719 |
+
inputs: { upscale_method: upscaleMethods[1], scale_by: 1, image: [`${image.id}`, 0] },
|
| 720 |
+
class_type: "ImageScaleBy",
|
| 721 |
+
},
|
| 722 |
+
[scale2id]: {
|
| 723 |
+
inputs: { upscale_method: "nearest-exact", scale_by: 1, image: [`${image.id}`, 0] },
|
| 724 |
+
class_type: "ImageScaleBy",
|
| 725 |
+
},
|
| 726 |
+
[preview1.id]: { inputs: { images: [`${scale1id}`, 0] }, class_type: "PreviewImage" },
|
| 727 |
+
[preview2.id]: { inputs: { images: [`${scale2id}`, 0] }, class_type: "PreviewImage" },
|
| 728 |
+
});
|
| 729 |
+
});
|
| 730 |
+
});
|
| 731 |
+
test("adds widgets in node execution order", async () => {
|
| 732 |
+
const { ez, graph, app } = await start();
|
| 733 |
+
const scale = ez.LatentUpscale();
|
| 734 |
+
const save = ez.SaveImage();
|
| 735 |
+
const empty = ez.EmptyLatentImage();
|
| 736 |
+
const decode = ez.VAEDecode();
|
| 737 |
+
|
| 738 |
+
scale.outputs.LATENT.connectTo(decode.inputs.samples);
|
| 739 |
+
decode.outputs.IMAGE.connectTo(save.inputs.images);
|
| 740 |
+
empty.outputs.LATENT.connectTo(scale.inputs.samples);
|
| 741 |
+
|
| 742 |
+
const group = await convertToGroup(app, graph, "test", [scale, save, empty, decode]);
|
| 743 |
+
const widgets = group.widgets.map((w) => w.widget.name);
|
| 744 |
+
expect(widgets).toStrictEqual([
|
| 745 |
+
"width",
|
| 746 |
+
"height",
|
| 747 |
+
"batch_size",
|
| 748 |
+
"upscale_method",
|
| 749 |
+
"LatentUpscale width",
|
| 750 |
+
"LatentUpscale height",
|
| 751 |
+
"crop",
|
| 752 |
+
"filename_prefix",
|
| 753 |
+
]);
|
| 754 |
+
});
|
| 755 |
+
test("adds output for external links when converting to group", async () => {
|
| 756 |
+
const { ez, graph, app } = await start();
|
| 757 |
+
const img = ez.EmptyLatentImage();
|
| 758 |
+
let decode = ez.VAEDecode(...img.outputs);
|
| 759 |
+
const preview1 = ez.PreviewImage(...decode.outputs);
|
| 760 |
+
const preview2 = ez.PreviewImage(...decode.outputs);
|
| 761 |
+
|
| 762 |
+
const group = await convertToGroup(app, graph, "test", [img, decode, preview1]);
|
| 763 |
+
|
| 764 |
+
// Ensure we have an output connected to the 2nd preview node
|
| 765 |
+
expect(group.outputs.length).toBe(1);
|
| 766 |
+
expect(group.outputs[0].connections.length).toBe(1);
|
| 767 |
+
expect(group.outputs[0].connections[0].targetNode.id).toBe(preview2.id);
|
| 768 |
+
|
| 769 |
+
// Convert back and ensure bothe previews are still connected
|
| 770 |
+
group.menu["Convert to nodes"].call();
|
| 771 |
+
decode = graph.find(decode);
|
| 772 |
+
expect(decode.outputs[0].connections.length).toBe(2);
|
| 773 |
+
expect(decode.outputs[0].connections[0].targetNode.id).toBe(preview1.id);
|
| 774 |
+
expect(decode.outputs[0].connections[1].targetNode.id).toBe(preview2.id);
|
| 775 |
+
});
|
| 776 |
+
test("adds output for external links when converting to group when nodes are not in execution order", async () => {
|
| 777 |
+
const { ez, graph, app } = await start();
|
| 778 |
+
const sampler = ez.KSampler();
|
| 779 |
+
const ckpt = ez.CheckpointLoaderSimple();
|
| 780 |
+
const empty = ez.EmptyLatentImage();
|
| 781 |
+
const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" });
|
| 782 |
+
const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" });
|
| 783 |
+
const decode1 = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE);
|
| 784 |
+
const save = ez.SaveImage(decode1.outputs.IMAGE);
|
| 785 |
+
ckpt.outputs.MODEL.connectTo(sampler.inputs.model);
|
| 786 |
+
pos.outputs.CONDITIONING.connectTo(sampler.inputs.positive);
|
| 787 |
+
neg.outputs.CONDITIONING.connectTo(sampler.inputs.negative);
|
| 788 |
+
empty.outputs.LATENT.connectTo(sampler.inputs.latent_image);
|
| 789 |
+
|
| 790 |
+
const encode = ez.VAEEncode(decode1.outputs.IMAGE);
|
| 791 |
+
const vae = ez.VAELoader();
|
| 792 |
+
const decode2 = ez.VAEDecode(encode.outputs.LATENT, vae.outputs.VAE);
|
| 793 |
+
const preview = ez.PreviewImage(decode2.outputs.IMAGE);
|
| 794 |
+
vae.outputs.VAE.connectTo(encode.inputs.vae);
|
| 795 |
+
|
| 796 |
+
const group = await convertToGroup(app, graph, "test", [vae, decode1, encode, sampler]);
|
| 797 |
+
|
| 798 |
+
expect(group.outputs.length).toBe(3);
|
| 799 |
+
expect(group.outputs[0].output.name).toBe("VAE");
|
| 800 |
+
expect(group.outputs[0].output.type).toBe("VAE");
|
| 801 |
+
expect(group.outputs[1].output.name).toBe("IMAGE");
|
| 802 |
+
expect(group.outputs[1].output.type).toBe("IMAGE");
|
| 803 |
+
expect(group.outputs[2].output.name).toBe("LATENT");
|
| 804 |
+
expect(group.outputs[2].output.type).toBe("LATENT");
|
| 805 |
+
|
| 806 |
+
expect(group.outputs[0].connections.length).toBe(1);
|
| 807 |
+
expect(group.outputs[0].connections[0].targetNode.id).toBe(decode2.id);
|
| 808 |
+
expect(group.outputs[0].connections[0].targetInput.index).toBe(1);
|
| 809 |
+
|
| 810 |
+
expect(group.outputs[1].connections.length).toBe(1);
|
| 811 |
+
expect(group.outputs[1].connections[0].targetNode.id).toBe(save.id);
|
| 812 |
+
expect(group.outputs[1].connections[0].targetInput.index).toBe(0);
|
| 813 |
+
|
| 814 |
+
expect(group.outputs[2].connections.length).toBe(1);
|
| 815 |
+
expect(group.outputs[2].connections[0].targetNode.id).toBe(decode2.id);
|
| 816 |
+
expect(group.outputs[2].connections[0].targetInput.index).toBe(0);
|
| 817 |
+
|
| 818 |
+
expect((await graph.toPrompt()).output).toEqual({
|
| 819 |
+
...getOutput({ 1: ckpt.id, 2: pos.id, 3: neg.id, 4: empty.id, 5: sampler.id, 6: decode1.id, 7: save.id }),
|
| 820 |
+
[vae.id]: { inputs: { vae_name: "vae1.safetensors" }, class_type: vae.node.type },
|
| 821 |
+
[encode.id]: { inputs: { pixels: ["6", 0], vae: [vae.id + "", 0] }, class_type: encode.node.type },
|
| 822 |
+
[decode2.id]: { inputs: { samples: [encode.id + "", 0], vae: [vae.id + "", 0] }, class_type: decode2.node.type },
|
| 823 |
+
[preview.id]: { inputs: { images: [decode2.id + "", 0] }, class_type: preview.node.type },
|
| 824 |
+
});
|
| 825 |
+
});
|
| 826 |
+
test("works with IMAGEUPLOAD widget", async () => {
|
| 827 |
+
const { ez, graph, app } = await start();
|
| 828 |
+
const img = ez.LoadImage();
|
| 829 |
+
const preview1 = ez.PreviewImage(img.outputs[0]);
|
| 830 |
+
|
| 831 |
+
const group = await convertToGroup(app, graph, "test", [img, preview1]);
|
| 832 |
+
const widget = group.widgets["upload"];
|
| 833 |
+
expect(widget).toBeTruthy();
|
| 834 |
+
expect(widget.widget.type).toBe("button");
|
| 835 |
+
});
|
| 836 |
+
test("internal primitive populates widgets for all linked inputs", async () => {
|
| 837 |
+
const { ez, graph, app } = await start();
|
| 838 |
+
const img = ez.LoadImage();
|
| 839 |
+
const scale1 = ez.ImageScale(img.outputs[0]);
|
| 840 |
+
const scale2 = ez.ImageScale(img.outputs[0]);
|
| 841 |
+
ez.PreviewImage(scale1.outputs[0]);
|
| 842 |
+
ez.PreviewImage(scale2.outputs[0]);
|
| 843 |
+
|
| 844 |
+
scale1.widgets.width.convertToInput();
|
| 845 |
+
scale2.widgets.height.convertToInput();
|
| 846 |
+
|
| 847 |
+
const primitive = ez.PrimitiveNode();
|
| 848 |
+
primitive.outputs[0].connectTo(scale1.inputs.width);
|
| 849 |
+
primitive.outputs[0].connectTo(scale2.inputs.height);
|
| 850 |
+
|
| 851 |
+
const group = await convertToGroup(app, graph, "test", [img, primitive, scale1, scale2]);
|
| 852 |
+
group.widgets.value.value = 100;
|
| 853 |
+
expect((await graph.toPrompt()).output).toEqual({
|
| 854 |
+
1: {
|
| 855 |
+
inputs: { image: img.widgets.image.value, upload: "image" },
|
| 856 |
+
class_type: "LoadImage",
|
| 857 |
+
},
|
| 858 |
+
2: {
|
| 859 |
+
inputs: { upscale_method: "nearest-exact", width: 100, height: 512, crop: "disabled", image: ["1", 0] },
|
| 860 |
+
class_type: "ImageScale",
|
| 861 |
+
},
|
| 862 |
+
3: {
|
| 863 |
+
inputs: { upscale_method: "nearest-exact", width: 512, height: 100, crop: "disabled", image: ["1", 0] },
|
| 864 |
+
class_type: "ImageScale",
|
| 865 |
+
},
|
| 866 |
+
4: { inputs: { images: ["2", 0] }, class_type: "PreviewImage" },
|
| 867 |
+
5: { inputs: { images: ["3", 0] }, class_type: "PreviewImage" },
|
| 868 |
+
});
|
| 869 |
+
});
|
| 870 |
+
test("primitive control widgets values are copied on convert", async () => {
|
| 871 |
+
const { ez, graph, app } = await start();
|
| 872 |
+
const sampler = ez.KSampler();
|
| 873 |
+
sampler.widgets.seed.convertToInput();
|
| 874 |
+
sampler.widgets.sampler_name.convertToInput();
|
| 875 |
+
|
| 876 |
+
let p1 = ez.PrimitiveNode();
|
| 877 |
+
let p2 = ez.PrimitiveNode();
|
| 878 |
+
p1.outputs[0].connectTo(sampler.inputs.seed);
|
| 879 |
+
p2.outputs[0].connectTo(sampler.inputs.sampler_name);
|
| 880 |
+
|
| 881 |
+
p1.widgets.control_after_generate.value = "increment";
|
| 882 |
+
p2.widgets.control_after_generate.value = "decrement";
|
| 883 |
+
p2.widgets.control_filter_list.value = "/.*/";
|
| 884 |
+
|
| 885 |
+
p2.node.title = "p2";
|
| 886 |
+
|
| 887 |
+
const group = await convertToGroup(app, graph, "test", [sampler, p1, p2]);
|
| 888 |
+
expect(group.widgets.control_after_generate.value).toBe("increment");
|
| 889 |
+
expect(group.widgets["p2 control_after_generate"].value).toBe("decrement");
|
| 890 |
+
expect(group.widgets["p2 control_filter_list"].value).toBe("/.*/");
|
| 891 |
+
|
| 892 |
+
group.widgets.control_after_generate.value = "fixed";
|
| 893 |
+
group.widgets["p2 control_after_generate"].value = "randomize";
|
| 894 |
+
group.widgets["p2 control_filter_list"].value = "/.+/";
|
| 895 |
+
|
| 896 |
+
group.menu["Convert to nodes"].call();
|
| 897 |
+
p1 = graph.find(p1);
|
| 898 |
+
p2 = graph.find(p2);
|
| 899 |
+
|
| 900 |
+
expect(p1.widgets.control_after_generate.value).toBe("fixed");
|
| 901 |
+
expect(p2.widgets.control_after_generate.value).toBe("randomize");
|
| 902 |
+
expect(p2.widgets.control_filter_list.value).toBe("/.+/");
|
| 903 |
+
});
|
| 904 |
+
test("internal reroutes work with converted inputs and merge options", async () => {
|
| 905 |
+
const { ez, graph, app } = await start();
|
| 906 |
+
const vae = ez.VAELoader();
|
| 907 |
+
const latent = ez.EmptyLatentImage();
|
| 908 |
+
const decode = ez.VAEDecode(latent.outputs.LATENT, vae.outputs.VAE);
|
| 909 |
+
const scale = ez.ImageScale(decode.outputs.IMAGE);
|
| 910 |
+
ez.PreviewImage(scale.outputs.IMAGE);
|
| 911 |
+
|
| 912 |
+
const r1 = ez.Reroute();
|
| 913 |
+
const r2 = ez.Reroute();
|
| 914 |
+
|
| 915 |
+
latent.widgets.width.value = 64;
|
| 916 |
+
latent.widgets.height.value = 128;
|
| 917 |
+
|
| 918 |
+
latent.widgets.width.convertToInput();
|
| 919 |
+
latent.widgets.height.convertToInput();
|
| 920 |
+
latent.widgets.batch_size.convertToInput();
|
| 921 |
+
|
| 922 |
+
scale.widgets.width.convertToInput();
|
| 923 |
+
scale.widgets.height.convertToInput();
|
| 924 |
+
|
| 925 |
+
r1.inputs[0].input.label = "hbw";
|
| 926 |
+
r1.outputs[0].connectTo(latent.inputs.height);
|
| 927 |
+
r1.outputs[0].connectTo(latent.inputs.batch_size);
|
| 928 |
+
r1.outputs[0].connectTo(scale.inputs.width);
|
| 929 |
+
|
| 930 |
+
r2.inputs[0].input.label = "wh";
|
| 931 |
+
r2.outputs[0].connectTo(latent.inputs.width);
|
| 932 |
+
r2.outputs[0].connectTo(scale.inputs.height);
|
| 933 |
+
|
| 934 |
+
const group = await convertToGroup(app, graph, "test", [r1, r2, latent, decode, scale]);
|
| 935 |
+
|
| 936 |
+
expect(group.inputs[0].input.type).toBe("VAE");
|
| 937 |
+
expect(group.inputs[1].input.type).toBe("INT");
|
| 938 |
+
expect(group.inputs[2].input.type).toBe("INT");
|
| 939 |
+
|
| 940 |
+
const p1 = ez.PrimitiveNode();
|
| 941 |
+
const p2 = ez.PrimitiveNode();
|
| 942 |
+
p1.outputs[0].connectTo(group.inputs[1]);
|
| 943 |
+
p2.outputs[0].connectTo(group.inputs[2]);
|
| 944 |
+
|
| 945 |
+
expect(p1.widgets.value.widget.options?.min).toBe(16); // width/height min
|
| 946 |
+
expect(p1.widgets.value.widget.options?.max).toBe(4096); // batch max
|
| 947 |
+
expect(p1.widgets.value.widget.options?.step).toBe(80); // width/height step * 10
|
| 948 |
+
|
| 949 |
+
expect(p2.widgets.value.widget.options?.min).toBe(16); // width/height min
|
| 950 |
+
expect(p2.widgets.value.widget.options?.max).toBe(16384); // width/height max
|
| 951 |
+
expect(p2.widgets.value.widget.options?.step).toBe(80); // width/height step * 10
|
| 952 |
+
|
| 953 |
+
expect(p1.widgets.value.value).toBe(128);
|
| 954 |
+
expect(p2.widgets.value.value).toBe(64);
|
| 955 |
+
|
| 956 |
+
p1.widgets.value.value = 16;
|
| 957 |
+
p2.widgets.value.value = 32;
|
| 958 |
+
|
| 959 |
+
await checkBeforeAndAfterReload(graph, async (r) => {
|
| 960 |
+
const id = (v) => (r ? `${group.id}:` : "") + v;
|
| 961 |
+
expect((await graph.toPrompt()).output).toStrictEqual({
|
| 962 |
+
1: { inputs: { vae_name: "vae1.safetensors" }, class_type: "VAELoader" },
|
| 963 |
+
[id(2)]: { inputs: { width: 32, height: 16, batch_size: 16 }, class_type: "EmptyLatentImage" },
|
| 964 |
+
[id(3)]: { inputs: { samples: [id(2), 0], vae: ["1", 0] }, class_type: "VAEDecode" },
|
| 965 |
+
[id(4)]: {
|
| 966 |
+
inputs: { upscale_method: "nearest-exact", width: 16, height: 32, crop: "disabled", image: [id(3), 0] },
|
| 967 |
+
class_type: "ImageScale",
|
| 968 |
+
},
|
| 969 |
+
5: { inputs: { images: [id(4), 0] }, class_type: "PreviewImage" },
|
| 970 |
+
});
|
| 971 |
+
});
|
| 972 |
+
});
|
| 973 |
+
test("converted inputs with linked widgets map values correctly on creation", async () => {
|
| 974 |
+
const { ez, graph, app } = await start();
|
| 975 |
+
const k1 = ez.KSampler();
|
| 976 |
+
const k2 = ez.KSampler();
|
| 977 |
+
k1.widgets.seed.convertToInput();
|
| 978 |
+
k2.widgets.seed.convertToInput();
|
| 979 |
+
|
| 980 |
+
const rr = ez.Reroute();
|
| 981 |
+
rr.outputs[0].connectTo(k1.inputs.seed);
|
| 982 |
+
rr.outputs[0].connectTo(k2.inputs.seed);
|
| 983 |
+
|
| 984 |
+
const group = await convertToGroup(app, graph, "test", [k1, k2, rr]);
|
| 985 |
+
expect(group.widgets.steps.value).toBe(20);
|
| 986 |
+
expect(group.widgets.cfg.value).toBe(8);
|
| 987 |
+
expect(group.widgets.scheduler.value).toBe("normal");
|
| 988 |
+
expect(group.widgets["KSampler steps"].value).toBe(20);
|
| 989 |
+
expect(group.widgets["KSampler cfg"].value).toBe(8);
|
| 990 |
+
expect(group.widgets["KSampler scheduler"].value).toBe("normal");
|
| 991 |
+
});
|
| 992 |
+
test("allow multiple of the same node type to be added", async () => {
|
| 993 |
+
const { ez, graph, app } = await start();
|
| 994 |
+
const nodes = [...Array(10)].map(() => ez.ImageScaleBy());
|
| 995 |
+
const group = await convertToGroup(app, graph, "test", nodes);
|
| 996 |
+
expect(group.inputs.length).toBe(10);
|
| 997 |
+
expect(group.outputs.length).toBe(10);
|
| 998 |
+
expect(group.widgets.length).toBe(20);
|
| 999 |
+
expect(group.widgets.map((w) => w.widget.name)).toStrictEqual(
|
| 1000 |
+
[...Array(10)]
|
| 1001 |
+
.map((_, i) => `${i > 0 ? "ImageScaleBy " : ""}${i > 1 ? i + " " : ""}`)
|
| 1002 |
+
.flatMap((p) => [`${p}upscale_method`, `${p}scale_by`])
|
| 1003 |
+
);
|
| 1004 |
+
});
|
| 1005 |
+
});
|
ComfyUI/tests-ui/tests/users.test.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
/// <reference path="../node_modules/@types/jest/index.d.ts" />
|
| 3 |
+
const { start } = require("../utils");
|
| 4 |
+
const lg = require("../utils/litegraph");
|
| 5 |
+
|
| 6 |
+
describe("users", () => {
|
| 7 |
+
beforeEach(() => {
|
| 8 |
+
lg.setup(global);
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
afterEach(() => {
|
| 12 |
+
lg.teardown(global);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
function expectNoUserScreen() {
|
| 16 |
+
// Ensure login isnt visible
|
| 17 |
+
const selection = document.querySelectorAll("#comfy-user-selection")?.[0];
|
| 18 |
+
expect(selection["style"].display).toBe("none");
|
| 19 |
+
const menu = document.querySelectorAll(".comfy-menu")?.[0];
|
| 20 |
+
expect(window.getComputedStyle(menu)?.display).not.toBe("none");
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
describe("multi-user", () => {
|
| 24 |
+
function mockAddStylesheet() {
|
| 25 |
+
const utils = require("../../web/scripts/utils");
|
| 26 |
+
utils.addStylesheet = jest.fn().mockReturnValue(Promise.resolve());
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
async function waitForUserScreenShow() {
|
| 30 |
+
mockAddStylesheet();
|
| 31 |
+
|
| 32 |
+
// Wait for "show" to be called
|
| 33 |
+
const { UserSelectionScreen } = require("../../web/scripts/ui/userSelection");
|
| 34 |
+
let resolve, reject;
|
| 35 |
+
const fn = UserSelectionScreen.prototype.show;
|
| 36 |
+
const p = new Promise((res, rej) => {
|
| 37 |
+
resolve = res;
|
| 38 |
+
reject = rej;
|
| 39 |
+
});
|
| 40 |
+
jest.spyOn(UserSelectionScreen.prototype, "show").mockImplementation(async (...args) => {
|
| 41 |
+
const res = fn(...args);
|
| 42 |
+
await new Promise(process.nextTick); // wait for promises to resolve
|
| 43 |
+
resolve();
|
| 44 |
+
return res;
|
| 45 |
+
});
|
| 46 |
+
// @ts-ignore
|
| 47 |
+
setTimeout(() => reject("timeout waiting for UserSelectionScreen to be shown."), 500);
|
| 48 |
+
await p;
|
| 49 |
+
await new Promise(process.nextTick); // wait for promises to resolve
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
async function testUserScreen(onShown, users) {
|
| 53 |
+
if (!users) {
|
| 54 |
+
users = {};
|
| 55 |
+
}
|
| 56 |
+
const starting = start({
|
| 57 |
+
resetEnv: true,
|
| 58 |
+
userConfig: { storage: "server", users },
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
// Ensure no current user
|
| 62 |
+
expect(localStorage["Comfy.userId"]).toBeFalsy();
|
| 63 |
+
expect(localStorage["Comfy.userName"]).toBeFalsy();
|
| 64 |
+
|
| 65 |
+
await waitForUserScreenShow();
|
| 66 |
+
|
| 67 |
+
const selection = document.querySelectorAll("#comfy-user-selection")?.[0];
|
| 68 |
+
expect(selection).toBeTruthy();
|
| 69 |
+
|
| 70 |
+
// Ensure login is visible
|
| 71 |
+
expect(window.getComputedStyle(selection)?.display).not.toBe("none");
|
| 72 |
+
// Ensure menu is hidden
|
| 73 |
+
const menu = document.querySelectorAll(".comfy-menu")?.[0];
|
| 74 |
+
expect(window.getComputedStyle(menu)?.display).toBe("none");
|
| 75 |
+
|
| 76 |
+
const isCreate = await onShown(selection);
|
| 77 |
+
|
| 78 |
+
// Submit form
|
| 79 |
+
selection.querySelectorAll("form")[0].submit();
|
| 80 |
+
await new Promise(process.nextTick); // wait for promises to resolve
|
| 81 |
+
|
| 82 |
+
// Wait for start
|
| 83 |
+
const s = await starting;
|
| 84 |
+
|
| 85 |
+
// Ensure login is removed
|
| 86 |
+
expect(document.querySelectorAll("#comfy-user-selection")).toHaveLength(0);
|
| 87 |
+
expect(window.getComputedStyle(menu)?.display).not.toBe("none");
|
| 88 |
+
|
| 89 |
+
// Ensure settings + templates are saved
|
| 90 |
+
const { api } = require("../../web/scripts/api");
|
| 91 |
+
expect(api.createUser).toHaveBeenCalledTimes(+isCreate);
|
| 92 |
+
expect(api.storeSettings).toHaveBeenCalledTimes(+isCreate);
|
| 93 |
+
expect(api.storeUserData).toHaveBeenCalledTimes(+isCreate);
|
| 94 |
+
if (isCreate) {
|
| 95 |
+
expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false });
|
| 96 |
+
expect(s.app.isNewUserSession).toBeTruthy();
|
| 97 |
+
} else {
|
| 98 |
+
expect(s.app.isNewUserSession).toBeFalsy();
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return { users, selection, ...s };
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
it("allows user creation if no users", async () => {
|
| 105 |
+
const { users } = await testUserScreen((selection) => {
|
| 106 |
+
// Ensure we have no users flag added
|
| 107 |
+
expect(selection.classList.contains("no-users")).toBeTruthy();
|
| 108 |
+
|
| 109 |
+
// Enter a username
|
| 110 |
+
const input = selection.getElementsByTagName("input")[0];
|
| 111 |
+
input.focus();
|
| 112 |
+
input.value = "Test User";
|
| 113 |
+
|
| 114 |
+
return true;
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
expect(users).toStrictEqual({
|
| 118 |
+
"Test User!": "Test User",
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
expect(localStorage["Comfy.userId"]).toBe("Test User!");
|
| 122 |
+
expect(localStorage["Comfy.userName"]).toBe("Test User");
|
| 123 |
+
});
|
| 124 |
+
it("allows user creation if no current user but other users", async () => {
|
| 125 |
+
const users = {
|
| 126 |
+
"Test User 2!": "Test User 2",
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
await testUserScreen((selection) => {
|
| 130 |
+
expect(selection.classList.contains("no-users")).toBeFalsy();
|
| 131 |
+
|
| 132 |
+
// Enter a username
|
| 133 |
+
const input = selection.getElementsByTagName("input")[0];
|
| 134 |
+
input.focus();
|
| 135 |
+
input.value = "Test User 3";
|
| 136 |
+
return true;
|
| 137 |
+
}, users);
|
| 138 |
+
|
| 139 |
+
expect(users).toStrictEqual({
|
| 140 |
+
"Test User 2!": "Test User 2",
|
| 141 |
+
"Test User 3!": "Test User 3",
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
expect(localStorage["Comfy.userId"]).toBe("Test User 3!");
|
| 145 |
+
expect(localStorage["Comfy.userName"]).toBe("Test User 3");
|
| 146 |
+
});
|
| 147 |
+
it("allows user selection if no current user but other users", async () => {
|
| 148 |
+
const users = {
|
| 149 |
+
"A!": "A",
|
| 150 |
+
"B!": "B",
|
| 151 |
+
"C!": "C",
|
| 152 |
+
};
|
| 153 |
+
|
| 154 |
+
await testUserScreen((selection) => {
|
| 155 |
+
expect(selection.classList.contains("no-users")).toBeFalsy();
|
| 156 |
+
|
| 157 |
+
// Check user list
|
| 158 |
+
const select = selection.getElementsByTagName("select")[0];
|
| 159 |
+
const options = select.getElementsByTagName("option");
|
| 160 |
+
expect(
|
| 161 |
+
[...options]
|
| 162 |
+
.filter((o) => !o.disabled)
|
| 163 |
+
.reduce((p, n) => {
|
| 164 |
+
p[n.getAttribute("value")] = n.textContent;
|
| 165 |
+
return p;
|
| 166 |
+
}, {})
|
| 167 |
+
).toStrictEqual(users);
|
| 168 |
+
|
| 169 |
+
// Select an option
|
| 170 |
+
select.focus();
|
| 171 |
+
select.value = options[2].value;
|
| 172 |
+
|
| 173 |
+
return false;
|
| 174 |
+
}, users);
|
| 175 |
+
|
| 176 |
+
expect(users).toStrictEqual(users);
|
| 177 |
+
|
| 178 |
+
expect(localStorage["Comfy.userId"]).toBe("B!");
|
| 179 |
+
expect(localStorage["Comfy.userName"]).toBe("B");
|
| 180 |
+
});
|
| 181 |
+
it("doesnt show user screen if current user", async () => {
|
| 182 |
+
const starting = start({
|
| 183 |
+
resetEnv: true,
|
| 184 |
+
userConfig: {
|
| 185 |
+
storage: "server",
|
| 186 |
+
users: {
|
| 187 |
+
"User!": "User",
|
| 188 |
+
},
|
| 189 |
+
},
|
| 190 |
+
localStorage: {
|
| 191 |
+
"Comfy.userId": "User!",
|
| 192 |
+
"Comfy.userName": "User",
|
| 193 |
+
},
|
| 194 |
+
});
|
| 195 |
+
await new Promise(process.nextTick); // wait for promises to resolve
|
| 196 |
+
|
| 197 |
+
expectNoUserScreen();
|
| 198 |
+
|
| 199 |
+
await starting;
|
| 200 |
+
});
|
| 201 |
+
it("allows user switching", async () => {
|
| 202 |
+
const { app } = await start({
|
| 203 |
+
resetEnv: true,
|
| 204 |
+
userConfig: {
|
| 205 |
+
storage: "server",
|
| 206 |
+
users: {
|
| 207 |
+
"User!": "User",
|
| 208 |
+
},
|
| 209 |
+
},
|
| 210 |
+
localStorage: {
|
| 211 |
+
"Comfy.userId": "User!",
|
| 212 |
+
"Comfy.userName": "User",
|
| 213 |
+
},
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
// cant actually test switching user easily but can check the setting is present
|
| 217 |
+
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeTruthy();
|
| 218 |
+
});
|
| 219 |
+
});
|
| 220 |
+
describe("single-user", () => {
|
| 221 |
+
it("doesnt show user creation if no default user", async () => {
|
| 222 |
+
const { app } = await start({
|
| 223 |
+
resetEnv: true,
|
| 224 |
+
userConfig: { migrated: false, storage: "server" },
|
| 225 |
+
});
|
| 226 |
+
expectNoUserScreen();
|
| 227 |
+
|
| 228 |
+
// It should store the settings
|
| 229 |
+
const { api } = require("../../web/scripts/api");
|
| 230 |
+
expect(api.storeSettings).toHaveBeenCalledTimes(1);
|
| 231 |
+
expect(api.storeUserData).toHaveBeenCalledTimes(1);
|
| 232 |
+
expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false });
|
| 233 |
+
expect(app.isNewUserSession).toBeTruthy();
|
| 234 |
+
});
|
| 235 |
+
it("doesnt show user creation if default user", async () => {
|
| 236 |
+
const { app } = await start({
|
| 237 |
+
resetEnv: true,
|
| 238 |
+
userConfig: { migrated: true, storage: "server" },
|
| 239 |
+
});
|
| 240 |
+
expectNoUserScreen();
|
| 241 |
+
|
| 242 |
+
// It should store the settings
|
| 243 |
+
const { api } = require("../../web/scripts/api");
|
| 244 |
+
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
| 245 |
+
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
| 246 |
+
expect(app.isNewUserSession).toBeFalsy();
|
| 247 |
+
});
|
| 248 |
+
it("doesnt allow user switching", async () => {
|
| 249 |
+
const { app } = await start({
|
| 250 |
+
resetEnv: true,
|
| 251 |
+
userConfig: { migrated: true, storage: "server" },
|
| 252 |
+
});
|
| 253 |
+
expectNoUserScreen();
|
| 254 |
+
|
| 255 |
+
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy();
|
| 256 |
+
});
|
| 257 |
+
});
|
| 258 |
+
describe("browser-user", () => {
|
| 259 |
+
it("doesnt show user creation if no default user", async () => {
|
| 260 |
+
const { app } = await start({
|
| 261 |
+
resetEnv: true,
|
| 262 |
+
userConfig: { migrated: false, storage: "browser" },
|
| 263 |
+
});
|
| 264 |
+
expectNoUserScreen();
|
| 265 |
+
|
| 266 |
+
// It should store the settings
|
| 267 |
+
const { api } = require("../../web/scripts/api");
|
| 268 |
+
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
| 269 |
+
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
| 270 |
+
expect(app.isNewUserSession).toBeFalsy();
|
| 271 |
+
});
|
| 272 |
+
it("doesnt show user creation if default user", async () => {
|
| 273 |
+
const { app } = await start({
|
| 274 |
+
resetEnv: true,
|
| 275 |
+
userConfig: { migrated: true, storage: "server" },
|
| 276 |
+
});
|
| 277 |
+
expectNoUserScreen();
|
| 278 |
+
|
| 279 |
+
// It should store the settings
|
| 280 |
+
const { api } = require("../../web/scripts/api");
|
| 281 |
+
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
| 282 |
+
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
| 283 |
+
expect(app.isNewUserSession).toBeFalsy();
|
| 284 |
+
});
|
| 285 |
+
it("doesnt allow user switching", async () => {
|
| 286 |
+
const { app } = await start({
|
| 287 |
+
resetEnv: true,
|
| 288 |
+
userConfig: { migrated: true, storage: "browser" },
|
| 289 |
+
});
|
| 290 |
+
expectNoUserScreen();
|
| 291 |
+
|
| 292 |
+
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy();
|
| 293 |
+
});
|
| 294 |
+
});
|
| 295 |
+
});
|
ComfyUI/tests-ui/tests/widgetInputs.test.js
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
/// <reference path="../node_modules/@types/jest/index.d.ts" />
|
| 3 |
+
|
| 4 |
+
const {
|
| 5 |
+
start,
|
| 6 |
+
makeNodeDef,
|
| 7 |
+
checkBeforeAndAfterReload,
|
| 8 |
+
assertNotNullOrUndefined,
|
| 9 |
+
createDefaultWorkflow,
|
| 10 |
+
} = require("../utils");
|
| 11 |
+
const lg = require("../utils/litegraph");
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* @typedef { import("../utils/ezgraph") } Ez
|
| 15 |
+
* @typedef { ReturnType<Ez["Ez"]["graph"]>["ez"] } EzNodeFactory
|
| 16 |
+
*/
|
| 17 |
+
|
| 18 |
+
/**
|
| 19 |
+
* @param { EzNodeFactory } ez
|
| 20 |
+
* @param { InstanceType<Ez["EzGraph"]> } graph
|
| 21 |
+
* @param { InstanceType<Ez["EzInput"]> } input
|
| 22 |
+
* @param { string } widgetType
|
| 23 |
+
* @param { number } controlWidgetCount
|
| 24 |
+
* @returns
|
| 25 |
+
*/
|
| 26 |
+
async function connectPrimitiveAndReload(ez, graph, input, widgetType, controlWidgetCount = 0) {
|
| 27 |
+
// Connect to primitive and ensure its still connected after
|
| 28 |
+
let primitive = ez.PrimitiveNode();
|
| 29 |
+
primitive.outputs[0].connectTo(input);
|
| 30 |
+
|
| 31 |
+
await checkBeforeAndAfterReload(graph, async () => {
|
| 32 |
+
primitive = graph.find(primitive);
|
| 33 |
+
let { connections } = primitive.outputs[0];
|
| 34 |
+
expect(connections).toHaveLength(1);
|
| 35 |
+
expect(connections[0].targetNode.id).toBe(input.node.node.id);
|
| 36 |
+
|
| 37 |
+
// Ensure widget is correct type
|
| 38 |
+
const valueWidget = primitive.widgets.value;
|
| 39 |
+
expect(valueWidget.widget.type).toBe(widgetType);
|
| 40 |
+
|
| 41 |
+
// Check if control_after_generate should be added
|
| 42 |
+
if (controlWidgetCount) {
|
| 43 |
+
const controlWidget = primitive.widgets.control_after_generate;
|
| 44 |
+
expect(controlWidget.widget.type).toBe("combo");
|
| 45 |
+
if (widgetType === "combo") {
|
| 46 |
+
const filterWidget = primitive.widgets.control_filter_list;
|
| 47 |
+
expect(filterWidget.widget.type).toBe("string");
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Ensure we dont have other widgets
|
| 52 |
+
expect(primitive.node.widgets).toHaveLength(1 + controlWidgetCount);
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
return primitive;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
describe("widget inputs", () => {
|
| 59 |
+
beforeEach(() => {
|
| 60 |
+
lg.setup(global);
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
afterEach(() => {
|
| 64 |
+
lg.teardown(global);
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
[
|
| 68 |
+
{ name: "int", type: "INT", widget: "number", control: 1 },
|
| 69 |
+
{ name: "float", type: "FLOAT", widget: "number", control: 1 },
|
| 70 |
+
{ name: "text", type: "STRING" },
|
| 71 |
+
{
|
| 72 |
+
name: "customtext",
|
| 73 |
+
type: "STRING",
|
| 74 |
+
opt: { multiline: true },
|
| 75 |
+
},
|
| 76 |
+
{ name: "toggle", type: "BOOLEAN" },
|
| 77 |
+
{ name: "combo", type: ["a", "b", "c"], control: 2 },
|
| 78 |
+
].forEach((c) => {
|
| 79 |
+
test(`widget conversion + primitive works on ${c.name}`, async () => {
|
| 80 |
+
const { ez, graph } = await start({
|
| 81 |
+
mockNodeDefs: makeNodeDef("TestNode", { [c.name]: [c.type, c.opt ?? {}] }),
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
// Create test node and convert to input
|
| 85 |
+
const n = ez.TestNode();
|
| 86 |
+
const w = n.widgets[c.name];
|
| 87 |
+
w.convertToInput();
|
| 88 |
+
expect(w.isConvertedToInput).toBeTruthy();
|
| 89 |
+
const input = w.getConvertedInput();
|
| 90 |
+
expect(input).toBeTruthy();
|
| 91 |
+
|
| 92 |
+
// @ts-ignore : input is valid here
|
| 93 |
+
await connectPrimitiveAndReload(ez, graph, input, c.widget ?? c.name, c.control);
|
| 94 |
+
});
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
test("converted widget works after reload", async () => {
|
| 98 |
+
const { ez, graph } = await start();
|
| 99 |
+
let n = ez.CheckpointLoaderSimple();
|
| 100 |
+
|
| 101 |
+
const inputCount = n.inputs.length;
|
| 102 |
+
|
| 103 |
+
// Convert ckpt name to an input
|
| 104 |
+
n.widgets.ckpt_name.convertToInput();
|
| 105 |
+
expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
|
| 106 |
+
expect(n.inputs.ckpt_name).toBeTruthy();
|
| 107 |
+
expect(n.inputs.length).toEqual(inputCount + 1);
|
| 108 |
+
|
| 109 |
+
// Convert back to widget and ensure input is removed
|
| 110 |
+
n.widgets.ckpt_name.convertToWidget();
|
| 111 |
+
expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
|
| 112 |
+
expect(n.inputs.ckpt_name).toBeFalsy();
|
| 113 |
+
expect(n.inputs.length).toEqual(inputCount);
|
| 114 |
+
|
| 115 |
+
// Convert again and reload the graph to ensure it maintains state
|
| 116 |
+
n.widgets.ckpt_name.convertToInput();
|
| 117 |
+
expect(n.inputs.length).toEqual(inputCount + 1);
|
| 118 |
+
|
| 119 |
+
const primitive = await connectPrimitiveAndReload(ez, graph, n.inputs.ckpt_name, "combo", 2);
|
| 120 |
+
|
| 121 |
+
// Disconnect & reconnect
|
| 122 |
+
primitive.outputs[0].connections[0].disconnect();
|
| 123 |
+
let { connections } = primitive.outputs[0];
|
| 124 |
+
expect(connections).toHaveLength(0);
|
| 125 |
+
|
| 126 |
+
primitive.outputs[0].connectTo(n.inputs.ckpt_name);
|
| 127 |
+
({ connections } = primitive.outputs[0]);
|
| 128 |
+
expect(connections).toHaveLength(1);
|
| 129 |
+
expect(connections[0].targetNode.id).toBe(n.node.id);
|
| 130 |
+
|
| 131 |
+
// Convert back to widget and ensure input is removed
|
| 132 |
+
n.widgets.ckpt_name.convertToWidget();
|
| 133 |
+
expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
|
| 134 |
+
expect(n.inputs.ckpt_name).toBeFalsy();
|
| 135 |
+
expect(n.inputs.length).toEqual(inputCount);
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
test("converted widget works on clone", async () => {
|
| 139 |
+
const { graph, ez } = await start();
|
| 140 |
+
let n = ez.CheckpointLoaderSimple();
|
| 141 |
+
|
| 142 |
+
// Convert the widget to an input
|
| 143 |
+
n.widgets.ckpt_name.convertToInput();
|
| 144 |
+
expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
|
| 145 |
+
|
| 146 |
+
// Clone the node
|
| 147 |
+
n.menu["Clone"].call();
|
| 148 |
+
expect(graph.nodes).toHaveLength(2);
|
| 149 |
+
const clone = graph.nodes[1];
|
| 150 |
+
expect(clone.id).not.toEqual(n.id);
|
| 151 |
+
|
| 152 |
+
// Ensure the clone has an input
|
| 153 |
+
expect(clone.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
|
| 154 |
+
expect(clone.inputs.ckpt_name).toBeTruthy();
|
| 155 |
+
|
| 156 |
+
// Ensure primitive connects to both nodes
|
| 157 |
+
let primitive = ez.PrimitiveNode();
|
| 158 |
+
primitive.outputs[0].connectTo(n.inputs.ckpt_name);
|
| 159 |
+
primitive.outputs[0].connectTo(clone.inputs.ckpt_name);
|
| 160 |
+
expect(primitive.outputs[0].connections).toHaveLength(2);
|
| 161 |
+
|
| 162 |
+
// Convert back to widget and ensure input is removed
|
| 163 |
+
clone.widgets.ckpt_name.convertToWidget();
|
| 164 |
+
expect(clone.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
|
| 165 |
+
expect(clone.inputs.ckpt_name).toBeFalsy();
|
| 166 |
+
});
|
| 167 |
+
|
| 168 |
+
test("shows missing node error on custom node with converted input", async () => {
|
| 169 |
+
const { graph } = await start();
|
| 170 |
+
|
| 171 |
+
const dialogShow = jest.spyOn(graph.app.ui.dialog, "show");
|
| 172 |
+
|
| 173 |
+
await graph.app.loadGraphData({
|
| 174 |
+
last_node_id: 3,
|
| 175 |
+
last_link_id: 4,
|
| 176 |
+
nodes: [
|
| 177 |
+
{
|
| 178 |
+
id: 1,
|
| 179 |
+
type: "TestNode",
|
| 180 |
+
pos: [41.87329101561909, 389.7381480823742],
|
| 181 |
+
size: { 0: 220, 1: 374 },
|
| 182 |
+
flags: {},
|
| 183 |
+
order: 1,
|
| 184 |
+
mode: 0,
|
| 185 |
+
inputs: [{ name: "test", type: "FLOAT", link: 4, widget: { name: "test" }, slot_index: 0 }],
|
| 186 |
+
outputs: [],
|
| 187 |
+
properties: { "Node name for S&R": "TestNode" },
|
| 188 |
+
widgets_values: [1],
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
id: 3,
|
| 192 |
+
type: "PrimitiveNode",
|
| 193 |
+
pos: [-312, 433],
|
| 194 |
+
size: { 0: 210, 1: 82 },
|
| 195 |
+
flags: {},
|
| 196 |
+
order: 0,
|
| 197 |
+
mode: 0,
|
| 198 |
+
outputs: [{ links: [4], widget: { name: "test" } }],
|
| 199 |
+
title: "test",
|
| 200 |
+
properties: {},
|
| 201 |
+
},
|
| 202 |
+
],
|
| 203 |
+
links: [[4, 3, 0, 1, 6, "FLOAT"]],
|
| 204 |
+
groups: [],
|
| 205 |
+
config: {},
|
| 206 |
+
extra: {},
|
| 207 |
+
version: 0.4,
|
| 208 |
+
});
|
| 209 |
+
|
| 210 |
+
expect(dialogShow).toBeCalledTimes(1);
|
| 211 |
+
expect(dialogShow.mock.calls[0][0].innerHTML).toContain("the following node types were not found");
|
| 212 |
+
expect(dialogShow.mock.calls[0][0].innerHTML).toContain("TestNode");
|
| 213 |
+
});
|
| 214 |
+
|
| 215 |
+
test("defaultInput widgets can be converted back to inputs", async () => {
|
| 216 |
+
const { graph, ez } = await start({
|
| 217 |
+
mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { defaultInput: true }] }),
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
// Create test node and ensure it starts as an input
|
| 221 |
+
let n = ez.TestNode();
|
| 222 |
+
let w = n.widgets.example;
|
| 223 |
+
expect(w.isConvertedToInput).toBeTruthy();
|
| 224 |
+
let input = w.getConvertedInput();
|
| 225 |
+
expect(input).toBeTruthy();
|
| 226 |
+
|
| 227 |
+
// Ensure it can be converted to
|
| 228 |
+
w.convertToWidget();
|
| 229 |
+
expect(w.isConvertedToInput).toBeFalsy();
|
| 230 |
+
expect(n.inputs.length).toEqual(0);
|
| 231 |
+
// and from
|
| 232 |
+
w.convertToInput();
|
| 233 |
+
expect(w.isConvertedToInput).toBeTruthy();
|
| 234 |
+
input = w.getConvertedInput();
|
| 235 |
+
|
| 236 |
+
// Reload and ensure it still only has 1 converted widget
|
| 237 |
+
if (!assertNotNullOrUndefined(input)) return;
|
| 238 |
+
|
| 239 |
+
await connectPrimitiveAndReload(ez, graph, input, "number", 1);
|
| 240 |
+
n = graph.find(n);
|
| 241 |
+
expect(n.widgets).toHaveLength(1);
|
| 242 |
+
w = n.widgets.example;
|
| 243 |
+
expect(w.isConvertedToInput).toBeTruthy();
|
| 244 |
+
|
| 245 |
+
// Convert back to widget and ensure it is still a widget after reload
|
| 246 |
+
w.convertToWidget();
|
| 247 |
+
await graph.reload();
|
| 248 |
+
n = graph.find(n);
|
| 249 |
+
expect(n.widgets).toHaveLength(1);
|
| 250 |
+
expect(n.widgets[0].isConvertedToInput).toBeFalsy();
|
| 251 |
+
expect(n.inputs.length).toEqual(0);
|
| 252 |
+
});
|
| 253 |
+
|
| 254 |
+
test("forceInput widgets can not be converted back to inputs", async () => {
|
| 255 |
+
const { graph, ez } = await start({
|
| 256 |
+
mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { forceInput: true }] }),
|
| 257 |
+
});
|
| 258 |
+
|
| 259 |
+
// Create test node and ensure it starts as an input
|
| 260 |
+
let n = ez.TestNode();
|
| 261 |
+
let w = n.widgets.example;
|
| 262 |
+
expect(w.isConvertedToInput).toBeTruthy();
|
| 263 |
+
const input = w.getConvertedInput();
|
| 264 |
+
expect(input).toBeTruthy();
|
| 265 |
+
|
| 266 |
+
// Convert to widget should error
|
| 267 |
+
expect(() => w.convertToWidget()).toThrow();
|
| 268 |
+
|
| 269 |
+
// Reload and ensure it still only has 1 converted widget
|
| 270 |
+
if (assertNotNullOrUndefined(input)) {
|
| 271 |
+
await connectPrimitiveAndReload(ez, graph, input, "number", 1);
|
| 272 |
+
n = graph.find(n);
|
| 273 |
+
expect(n.widgets).toHaveLength(1);
|
| 274 |
+
expect(n.widgets.example.isConvertedToInput).toBeTruthy();
|
| 275 |
+
}
|
| 276 |
+
});
|
| 277 |
+
|
| 278 |
+
test("primitive can connect to matching combos on converted widgets", async () => {
|
| 279 |
+
const { ez } = await start({
|
| 280 |
+
mockNodeDefs: {
|
| 281 |
+
...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }),
|
| 282 |
+
...makeNodeDef("TestNode2", { example: [["A", "B", "C"], { forceInput: true }] }),
|
| 283 |
+
},
|
| 284 |
+
});
|
| 285 |
+
|
| 286 |
+
const n1 = ez.TestNode1();
|
| 287 |
+
const n2 = ez.TestNode2();
|
| 288 |
+
const p = ez.PrimitiveNode();
|
| 289 |
+
p.outputs[0].connectTo(n1.inputs[0]);
|
| 290 |
+
p.outputs[0].connectTo(n2.inputs[0]);
|
| 291 |
+
expect(p.outputs[0].connections).toHaveLength(2);
|
| 292 |
+
const valueWidget = p.widgets.value;
|
| 293 |
+
expect(valueWidget.widget.type).toBe("combo");
|
| 294 |
+
expect(valueWidget.widget.options.values).toEqual(["A", "B", "C"]);
|
| 295 |
+
});
|
| 296 |
+
|
| 297 |
+
test("primitive can not connect to non matching combos on converted widgets", async () => {
|
| 298 |
+
const { ez } = await start({
|
| 299 |
+
mockNodeDefs: {
|
| 300 |
+
...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }),
|
| 301 |
+
...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }),
|
| 302 |
+
},
|
| 303 |
+
});
|
| 304 |
+
|
| 305 |
+
const n1 = ez.TestNode1();
|
| 306 |
+
const n2 = ez.TestNode2();
|
| 307 |
+
const p = ez.PrimitiveNode();
|
| 308 |
+
p.outputs[0].connectTo(n1.inputs[0]);
|
| 309 |
+
expect(() => p.outputs[0].connectTo(n2.inputs[0])).toThrow();
|
| 310 |
+
expect(p.outputs[0].connections).toHaveLength(1);
|
| 311 |
+
});
|
| 312 |
+
|
| 313 |
+
test("combo output can not connect to non matching combos list input", async () => {
|
| 314 |
+
const { ez } = await start({
|
| 315 |
+
mockNodeDefs: {
|
| 316 |
+
...makeNodeDef("TestNode1", {}, [["A", "B"]]),
|
| 317 |
+
...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }),
|
| 318 |
+
...makeNodeDef("TestNode3", { example: [["A", "B", "C"], { forceInput: true }] }),
|
| 319 |
+
},
|
| 320 |
+
});
|
| 321 |
+
|
| 322 |
+
const n1 = ez.TestNode1();
|
| 323 |
+
const n2 = ez.TestNode2();
|
| 324 |
+
const n3 = ez.TestNode3();
|
| 325 |
+
|
| 326 |
+
n1.outputs[0].connectTo(n2.inputs[0]);
|
| 327 |
+
expect(() => n1.outputs[0].connectTo(n3.inputs[0])).toThrow();
|
| 328 |
+
});
|
| 329 |
+
|
| 330 |
+
test("combo primitive can filter list when control_after_generate called", async () => {
|
| 331 |
+
const { ez } = await start({
|
| 332 |
+
mockNodeDefs: {
|
| 333 |
+
...makeNodeDef("TestNode1", { example: [["A", "B", "C", "D", "AA", "BB", "CC", "DD", "AAA", "BBB"], {}] }),
|
| 334 |
+
},
|
| 335 |
+
});
|
| 336 |
+
|
| 337 |
+
const n1 = ez.TestNode1();
|
| 338 |
+
n1.widgets.example.convertToInput();
|
| 339 |
+
const p = ez.PrimitiveNode();
|
| 340 |
+
p.outputs[0].connectTo(n1.inputs[0]);
|
| 341 |
+
|
| 342 |
+
const value = p.widgets.value;
|
| 343 |
+
const control = p.widgets.control_after_generate.widget;
|
| 344 |
+
const filter = p.widgets.control_filter_list;
|
| 345 |
+
|
| 346 |
+
expect(p.widgets.length).toBe(3);
|
| 347 |
+
control.value = "increment";
|
| 348 |
+
expect(value.value).toBe("A");
|
| 349 |
+
|
| 350 |
+
// Manually trigger after queue when set to increment
|
| 351 |
+
control["afterQueued"]();
|
| 352 |
+
expect(value.value).toBe("B");
|
| 353 |
+
|
| 354 |
+
// Filter to items containing D
|
| 355 |
+
filter.value = "D";
|
| 356 |
+
control["afterQueued"]();
|
| 357 |
+
expect(value.value).toBe("D");
|
| 358 |
+
control["afterQueued"]();
|
| 359 |
+
expect(value.value).toBe("DD");
|
| 360 |
+
|
| 361 |
+
// Check decrement
|
| 362 |
+
value.value = "BBB";
|
| 363 |
+
control.value = "decrement";
|
| 364 |
+
filter.value = "B";
|
| 365 |
+
control["afterQueued"]();
|
| 366 |
+
expect(value.value).toBe("BB");
|
| 367 |
+
control["afterQueued"]();
|
| 368 |
+
expect(value.value).toBe("B");
|
| 369 |
+
|
| 370 |
+
// Check regex works
|
| 371 |
+
value.value = "BBB";
|
| 372 |
+
filter.value = "/[AB]|^C$/";
|
| 373 |
+
control["afterQueued"]();
|
| 374 |
+
expect(value.value).toBe("AAA");
|
| 375 |
+
control["afterQueued"]();
|
| 376 |
+
expect(value.value).toBe("BB");
|
| 377 |
+
control["afterQueued"]();
|
| 378 |
+
expect(value.value).toBe("AA");
|
| 379 |
+
control["afterQueued"]();
|
| 380 |
+
expect(value.value).toBe("C");
|
| 381 |
+
control["afterQueued"]();
|
| 382 |
+
expect(value.value).toBe("B");
|
| 383 |
+
control["afterQueued"]();
|
| 384 |
+
expect(value.value).toBe("A");
|
| 385 |
+
|
| 386 |
+
// Check random
|
| 387 |
+
control.value = "randomize";
|
| 388 |
+
filter.value = "/D/";
|
| 389 |
+
for (let i = 0; i < 100; i++) {
|
| 390 |
+
control["afterQueued"]();
|
| 391 |
+
expect(value.value === "D" || value.value === "DD").toBeTruthy();
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
// Ensure it doesnt apply when fixed
|
| 395 |
+
control.value = "fixed";
|
| 396 |
+
value.value = "B";
|
| 397 |
+
filter.value = "C";
|
| 398 |
+
control["afterQueued"]();
|
| 399 |
+
expect(value.value).toBe("B");
|
| 400 |
+
});
|
| 401 |
+
|
| 402 |
+
describe("reroutes", () => {
|
| 403 |
+
async function checkOutput(graph, values) {
|
| 404 |
+
expect((await graph.toPrompt()).output).toStrictEqual({
|
| 405 |
+
1: { inputs: { ckpt_name: "model1.safetensors" }, class_type: "CheckpointLoaderSimple" },
|
| 406 |
+
2: { inputs: { text: "positive", clip: ["1", 1] }, class_type: "CLIPTextEncode" },
|
| 407 |
+
3: { inputs: { text: "negative", clip: ["1", 1] }, class_type: "CLIPTextEncode" },
|
| 408 |
+
4: {
|
| 409 |
+
inputs: { width: values.width ?? 512, height: values.height ?? 512, batch_size: values?.batch_size ?? 1 },
|
| 410 |
+
class_type: "EmptyLatentImage",
|
| 411 |
+
},
|
| 412 |
+
5: {
|
| 413 |
+
inputs: {
|
| 414 |
+
seed: 0,
|
| 415 |
+
steps: 20,
|
| 416 |
+
cfg: 8,
|
| 417 |
+
sampler_name: "euler",
|
| 418 |
+
scheduler: values?.scheduler ?? "normal",
|
| 419 |
+
denoise: 1,
|
| 420 |
+
model: ["1", 0],
|
| 421 |
+
positive: ["2", 0],
|
| 422 |
+
negative: ["3", 0],
|
| 423 |
+
latent_image: ["4", 0],
|
| 424 |
+
},
|
| 425 |
+
class_type: "KSampler",
|
| 426 |
+
},
|
| 427 |
+
6: { inputs: { samples: ["5", 0], vae: ["1", 2] }, class_type: "VAEDecode" },
|
| 428 |
+
7: {
|
| 429 |
+
inputs: { filename_prefix: values.filename_prefix ?? "ComfyUI", images: ["6", 0] },
|
| 430 |
+
class_type: "SaveImage",
|
| 431 |
+
},
|
| 432 |
+
});
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
async function waitForWidget(node) {
|
| 436 |
+
// widgets are created slightly after the graph is ready
|
| 437 |
+
// hard to find an exact hook to get these so just wait for them to be ready
|
| 438 |
+
for (let i = 0; i < 10; i++) {
|
| 439 |
+
await new Promise((r) => setTimeout(r, 10));
|
| 440 |
+
if (node.widgets?.value) {
|
| 441 |
+
return;
|
| 442 |
+
}
|
| 443 |
+
}
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
it("can connect primitive via a reroute path to a widget input", async () => {
|
| 447 |
+
const { ez, graph } = await start();
|
| 448 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 449 |
+
|
| 450 |
+
nodes.empty.widgets.width.convertToInput();
|
| 451 |
+
nodes.sampler.widgets.scheduler.convertToInput();
|
| 452 |
+
nodes.save.widgets.filename_prefix.convertToInput();
|
| 453 |
+
|
| 454 |
+
let widthReroute = ez.Reroute();
|
| 455 |
+
let schedulerReroute = ez.Reroute();
|
| 456 |
+
let fileReroute = ez.Reroute();
|
| 457 |
+
|
| 458 |
+
let widthNext = widthReroute;
|
| 459 |
+
let schedulerNext = schedulerReroute;
|
| 460 |
+
let fileNext = fileReroute;
|
| 461 |
+
|
| 462 |
+
for (let i = 0; i < 5; i++) {
|
| 463 |
+
let next = ez.Reroute();
|
| 464 |
+
widthNext.outputs[0].connectTo(next.inputs[0]);
|
| 465 |
+
widthNext = next;
|
| 466 |
+
|
| 467 |
+
next = ez.Reroute();
|
| 468 |
+
schedulerNext.outputs[0].connectTo(next.inputs[0]);
|
| 469 |
+
schedulerNext = next;
|
| 470 |
+
|
| 471 |
+
next = ez.Reroute();
|
| 472 |
+
fileNext.outputs[0].connectTo(next.inputs[0]);
|
| 473 |
+
fileNext = next;
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
widthNext.outputs[0].connectTo(nodes.empty.inputs.width);
|
| 477 |
+
schedulerNext.outputs[0].connectTo(nodes.sampler.inputs.scheduler);
|
| 478 |
+
fileNext.outputs[0].connectTo(nodes.save.inputs.filename_prefix);
|
| 479 |
+
|
| 480 |
+
let widthPrimitive = ez.PrimitiveNode();
|
| 481 |
+
let schedulerPrimitive = ez.PrimitiveNode();
|
| 482 |
+
let filePrimitive = ez.PrimitiveNode();
|
| 483 |
+
|
| 484 |
+
widthPrimitive.outputs[0].connectTo(widthReroute.inputs[0]);
|
| 485 |
+
schedulerPrimitive.outputs[0].connectTo(schedulerReroute.inputs[0]);
|
| 486 |
+
filePrimitive.outputs[0].connectTo(fileReroute.inputs[0]);
|
| 487 |
+
expect(widthPrimitive.widgets.value.value).toBe(512);
|
| 488 |
+
widthPrimitive.widgets.value.value = 1024;
|
| 489 |
+
expect(schedulerPrimitive.widgets.value.value).toBe("normal");
|
| 490 |
+
schedulerPrimitive.widgets.value.value = "simple";
|
| 491 |
+
expect(filePrimitive.widgets.value.value).toBe("ComfyUI");
|
| 492 |
+
filePrimitive.widgets.value.value = "ComfyTest";
|
| 493 |
+
|
| 494 |
+
await checkBeforeAndAfterReload(graph, async () => {
|
| 495 |
+
widthPrimitive = graph.find(widthPrimitive);
|
| 496 |
+
schedulerPrimitive = graph.find(schedulerPrimitive);
|
| 497 |
+
filePrimitive = graph.find(filePrimitive);
|
| 498 |
+
await waitForWidget(filePrimitive);
|
| 499 |
+
expect(widthPrimitive.widgets.length).toBe(2);
|
| 500 |
+
expect(schedulerPrimitive.widgets.length).toBe(3);
|
| 501 |
+
expect(filePrimitive.widgets.length).toBe(1);
|
| 502 |
+
|
| 503 |
+
await checkOutput(graph, {
|
| 504 |
+
width: 1024,
|
| 505 |
+
scheduler: "simple",
|
| 506 |
+
filename_prefix: "ComfyTest",
|
| 507 |
+
});
|
| 508 |
+
});
|
| 509 |
+
});
|
| 510 |
+
it("can connect primitive via a reroute path to multiple widget inputs", async () => {
|
| 511 |
+
const { ez, graph } = await start();
|
| 512 |
+
const nodes = createDefaultWorkflow(ez, graph);
|
| 513 |
+
|
| 514 |
+
nodes.empty.widgets.width.convertToInput();
|
| 515 |
+
nodes.empty.widgets.height.convertToInput();
|
| 516 |
+
nodes.empty.widgets.batch_size.convertToInput();
|
| 517 |
+
|
| 518 |
+
let reroute = ez.Reroute();
|
| 519 |
+
let prevReroute = reroute;
|
| 520 |
+
for (let i = 0; i < 5; i++) {
|
| 521 |
+
const next = ez.Reroute();
|
| 522 |
+
prevReroute.outputs[0].connectTo(next.inputs[0]);
|
| 523 |
+
prevReroute = next;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
const r1 = ez.Reroute(prevReroute.outputs[0]);
|
| 527 |
+
const r2 = ez.Reroute(prevReroute.outputs[0]);
|
| 528 |
+
const r3 = ez.Reroute(r2.outputs[0]);
|
| 529 |
+
const r4 = ez.Reroute(r2.outputs[0]);
|
| 530 |
+
|
| 531 |
+
r1.outputs[0].connectTo(nodes.empty.inputs.width);
|
| 532 |
+
r3.outputs[0].connectTo(nodes.empty.inputs.height);
|
| 533 |
+
r4.outputs[0].connectTo(nodes.empty.inputs.batch_size);
|
| 534 |
+
|
| 535 |
+
let primitive = ez.PrimitiveNode();
|
| 536 |
+
primitive.outputs[0].connectTo(reroute.inputs[0]);
|
| 537 |
+
expect(primitive.widgets.value.value).toBe(1);
|
| 538 |
+
primitive.widgets.value.value = 64;
|
| 539 |
+
|
| 540 |
+
await checkBeforeAndAfterReload(graph, async (r) => {
|
| 541 |
+
primitive = graph.find(primitive);
|
| 542 |
+
await waitForWidget(primitive);
|
| 543 |
+
|
| 544 |
+
// Ensure widget configs are merged
|
| 545 |
+
expect(primitive.widgets.value.widget.options?.min).toBe(16); // width/height min
|
| 546 |
+
expect(primitive.widgets.value.widget.options?.max).toBe(4096); // batch max
|
| 547 |
+
expect(primitive.widgets.value.widget.options?.step).toBe(80); // width/height step * 10
|
| 548 |
+
|
| 549 |
+
await checkOutput(graph, {
|
| 550 |
+
width: 64,
|
| 551 |
+
height: 64,
|
| 552 |
+
batch_size: 64,
|
| 553 |
+
});
|
| 554 |
+
});
|
| 555 |
+
});
|
| 556 |
+
});
|
| 557 |
+
});
|
ComfyUI/tests-ui/utils/ezgraph.js
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
/// <reference path="../../web/types/litegraph.d.ts" />
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* @typedef { import("../../web/scripts/app")["app"] } app
|
| 6 |
+
* @typedef { import("../../web/types/litegraph") } LG
|
| 7 |
+
* @typedef { import("../../web/types/litegraph").IWidget } IWidget
|
| 8 |
+
* @typedef { import("../../web/types/litegraph").ContextMenuItem } ContextMenuItem
|
| 9 |
+
* @typedef { import("../../web/types/litegraph").INodeInputSlot } INodeInputSlot
|
| 10 |
+
* @typedef { import("../../web/types/litegraph").INodeOutputSlot } INodeOutputSlot
|
| 11 |
+
* @typedef { InstanceType<LG["LGraphNode"]> & { widgets?: Array<IWidget> } } LGNode
|
| 12 |
+
* @typedef { (...args: EzOutput[] | [...EzOutput[], Record<string, unknown>]) => EzNode } EzNodeFactory
|
| 13 |
+
*/
|
| 14 |
+
|
| 15 |
+
export class EzConnection {
|
| 16 |
+
/** @type { app } */
|
| 17 |
+
app;
|
| 18 |
+
/** @type { InstanceType<LG["LLink"]> } */
|
| 19 |
+
link;
|
| 20 |
+
|
| 21 |
+
get originNode() {
|
| 22 |
+
return new EzNode(this.app, this.app.graph.getNodeById(this.link.origin_id));
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
get originOutput() {
|
| 26 |
+
return this.originNode.outputs[this.link.origin_slot];
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
get targetNode() {
|
| 30 |
+
return new EzNode(this.app, this.app.graph.getNodeById(this.link.target_id));
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
get targetInput() {
|
| 34 |
+
return this.targetNode.inputs[this.link.target_slot];
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* @param { app } app
|
| 39 |
+
* @param { InstanceType<LG["LLink"]> } link
|
| 40 |
+
*/
|
| 41 |
+
constructor(app, link) {
|
| 42 |
+
this.app = app;
|
| 43 |
+
this.link = link;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
disconnect() {
|
| 47 |
+
this.targetInput.disconnect();
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export class EzSlot {
|
| 52 |
+
/** @type { EzNode } */
|
| 53 |
+
node;
|
| 54 |
+
/** @type { number } */
|
| 55 |
+
index;
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* @param { EzNode } node
|
| 59 |
+
* @param { number } index
|
| 60 |
+
*/
|
| 61 |
+
constructor(node, index) {
|
| 62 |
+
this.node = node;
|
| 63 |
+
this.index = index;
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export class EzInput extends EzSlot {
|
| 68 |
+
/** @type { INodeInputSlot } */
|
| 69 |
+
input;
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* @param { EzNode } node
|
| 73 |
+
* @param { number } index
|
| 74 |
+
* @param { INodeInputSlot } input
|
| 75 |
+
*/
|
| 76 |
+
constructor(node, index, input) {
|
| 77 |
+
super(node, index);
|
| 78 |
+
this.input = input;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
get connection() {
|
| 82 |
+
const link = this.node.node.inputs?.[this.index]?.link;
|
| 83 |
+
if (link == null) {
|
| 84 |
+
return null;
|
| 85 |
+
}
|
| 86 |
+
return new EzConnection(this.node.app, this.node.app.graph.links[link]);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
disconnect() {
|
| 90 |
+
this.node.node.disconnectInput(this.index);
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
export class EzOutput extends EzSlot {
|
| 95 |
+
/** @type { INodeOutputSlot } */
|
| 96 |
+
output;
|
| 97 |
+
|
| 98 |
+
/**
|
| 99 |
+
* @param { EzNode } node
|
| 100 |
+
* @param { number } index
|
| 101 |
+
* @param { INodeOutputSlot } output
|
| 102 |
+
*/
|
| 103 |
+
constructor(node, index, output) {
|
| 104 |
+
super(node, index);
|
| 105 |
+
this.output = output;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
get connections() {
|
| 109 |
+
return (this.node.node.outputs?.[this.index]?.links ?? []).map(
|
| 110 |
+
(l) => new EzConnection(this.node.app, this.node.app.graph.links[l])
|
| 111 |
+
);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/**
|
| 115 |
+
* @param { EzInput } input
|
| 116 |
+
*/
|
| 117 |
+
connectTo(input) {
|
| 118 |
+
if (!input) throw new Error("Invalid input");
|
| 119 |
+
|
| 120 |
+
/**
|
| 121 |
+
* @type { LG["LLink"] | null }
|
| 122 |
+
*/
|
| 123 |
+
const link = this.node.node.connect(this.index, input.node.node, input.index);
|
| 124 |
+
if (!link) {
|
| 125 |
+
const inp = input.input;
|
| 126 |
+
const inName = inp.name || inp.label || inp.type;
|
| 127 |
+
throw new Error(
|
| 128 |
+
`Connecting from ${input.node.node.type}#${input.node.id}[${inName}#${input.index}] -> ${this.node.node.type}#${this.node.id}[${
|
| 129 |
+
this.output.name ?? this.output.type
|
| 130 |
+
}#${this.index}] failed.`
|
| 131 |
+
);
|
| 132 |
+
}
|
| 133 |
+
return link;
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
export class EzNodeMenuItem {
|
| 138 |
+
/** @type { EzNode } */
|
| 139 |
+
node;
|
| 140 |
+
/** @type { number } */
|
| 141 |
+
index;
|
| 142 |
+
/** @type { ContextMenuItem } */
|
| 143 |
+
item;
|
| 144 |
+
|
| 145 |
+
/**
|
| 146 |
+
* @param { EzNode } node
|
| 147 |
+
* @param { number } index
|
| 148 |
+
* @param { ContextMenuItem } item
|
| 149 |
+
*/
|
| 150 |
+
constructor(node, index, item) {
|
| 151 |
+
this.node = node;
|
| 152 |
+
this.index = index;
|
| 153 |
+
this.item = item;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
call(selectNode = true) {
|
| 157 |
+
if (!this.item?.callback) throw new Error(`Menu Item ${this.item?.content ?? "[null]"} has no callback.`);
|
| 158 |
+
if (selectNode) {
|
| 159 |
+
this.node.select();
|
| 160 |
+
}
|
| 161 |
+
return this.item.callback.call(this.node.node, undefined, undefined, undefined, undefined, this.node.node);
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
export class EzWidget {
|
| 166 |
+
/** @type { EzNode } */
|
| 167 |
+
node;
|
| 168 |
+
/** @type { number } */
|
| 169 |
+
index;
|
| 170 |
+
/** @type { IWidget } */
|
| 171 |
+
widget;
|
| 172 |
+
|
| 173 |
+
/**
|
| 174 |
+
* @param { EzNode } node
|
| 175 |
+
* @param { number } index
|
| 176 |
+
* @param { IWidget } widget
|
| 177 |
+
*/
|
| 178 |
+
constructor(node, index, widget) {
|
| 179 |
+
this.node = node;
|
| 180 |
+
this.index = index;
|
| 181 |
+
this.widget = widget;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
get value() {
|
| 185 |
+
return this.widget.value;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
set value(v) {
|
| 189 |
+
this.widget.value = v;
|
| 190 |
+
this.widget.callback?.call?.(this.widget, v)
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
get isConvertedToInput() {
|
| 194 |
+
// @ts-ignore : this type is valid for converted widgets
|
| 195 |
+
return this.widget.type === "converted-widget";
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
getConvertedInput() {
|
| 199 |
+
if (!this.isConvertedToInput) throw new Error(`Widget ${this.widget.name} is not converted to input.`);
|
| 200 |
+
|
| 201 |
+
return this.node.inputs.find((inp) => inp.input["widget"]?.name === this.widget.name);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
convertToWidget() {
|
| 205 |
+
if (!this.isConvertedToInput)
|
| 206 |
+
throw new Error(`Widget ${this.widget.name} cannot be converted as it is already a widget.`);
|
| 207 |
+
var menu = this.node.menu["Convert Input to Widget"].item.submenu.options;
|
| 208 |
+
var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to widget`);
|
| 209 |
+
menu[index].callback.call();
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
convertToInput() {
|
| 213 |
+
if (this.isConvertedToInput)
|
| 214 |
+
throw new Error(`Widget ${this.widget.name} cannot be converted as it is already an input.`);
|
| 215 |
+
var menu = this.node.menu["Convert Widget to Input"].item.submenu.options;
|
| 216 |
+
var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to input`);
|
| 217 |
+
menu[index].callback.call();
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
export class EzNode {
|
| 222 |
+
/** @type { app } */
|
| 223 |
+
app;
|
| 224 |
+
/** @type { LGNode } */
|
| 225 |
+
node;
|
| 226 |
+
|
| 227 |
+
/**
|
| 228 |
+
* @param { app } app
|
| 229 |
+
* @param { LGNode } node
|
| 230 |
+
*/
|
| 231 |
+
constructor(app, node) {
|
| 232 |
+
this.app = app;
|
| 233 |
+
this.node = node;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
get id() {
|
| 237 |
+
return this.node.id;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
get inputs() {
|
| 241 |
+
return this.#makeLookupArray("inputs", "name", EzInput);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
get outputs() {
|
| 245 |
+
return this.#makeLookupArray("outputs", "name", EzOutput);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
get widgets() {
|
| 249 |
+
return this.#makeLookupArray("widgets", "name", EzWidget);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
get menu() {
|
| 253 |
+
return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
get isRemoved() {
|
| 257 |
+
return !this.app.graph.getNodeById(this.id);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
select(addToSelection = false) {
|
| 261 |
+
this.app.canvas.selectNode(this.node, addToSelection);
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// /**
|
| 265 |
+
// * @template { "inputs" | "outputs" } T
|
| 266 |
+
// * @param { T } type
|
| 267 |
+
// * @returns { Record<string, type extends "inputs" ? EzInput : EzOutput> & (type extends "inputs" ? EzInput [] : EzOutput[]) }
|
| 268 |
+
// */
|
| 269 |
+
// #getSlotItems(type) {
|
| 270 |
+
// // @ts-ignore : these items are correct
|
| 271 |
+
// return (this.node[type] ?? []).reduce((p, s, i) => {
|
| 272 |
+
// if (s.name in p) {
|
| 273 |
+
// throw new Error(`Unable to store input ${s.name} on array as name conflicts.`);
|
| 274 |
+
// }
|
| 275 |
+
// // @ts-ignore
|
| 276 |
+
// p.push((p[s.name] = new (type === "inputs" ? EzInput : EzOutput)(this, i, s)));
|
| 277 |
+
// return p;
|
| 278 |
+
// }, Object.assign([], { $: this }));
|
| 279 |
+
// }
|
| 280 |
+
|
| 281 |
+
/**
|
| 282 |
+
* @template { { new(node: EzNode, index: number, obj: any): any } } T
|
| 283 |
+
* @param { "inputs" | "outputs" | "widgets" | (() => Array<unknown>) } nodeProperty
|
| 284 |
+
* @param { string } nameProperty
|
| 285 |
+
* @param { T } ctor
|
| 286 |
+
* @returns { Record<string, InstanceType<T>> & Array<InstanceType<T>> }
|
| 287 |
+
*/
|
| 288 |
+
#makeLookupArray(nodeProperty, nameProperty, ctor) {
|
| 289 |
+
const items = typeof nodeProperty === "function" ? nodeProperty() : this.node[nodeProperty];
|
| 290 |
+
// @ts-ignore
|
| 291 |
+
return (items ?? []).reduce((p, s, i) => {
|
| 292 |
+
if (!s) return p;
|
| 293 |
+
|
| 294 |
+
const name = s[nameProperty];
|
| 295 |
+
const item = new ctor(this, i, s);
|
| 296 |
+
// @ts-ignore
|
| 297 |
+
p.push(item);
|
| 298 |
+
if (name) {
|
| 299 |
+
// @ts-ignore
|
| 300 |
+
if (name in p) {
|
| 301 |
+
throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`);
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
// @ts-ignore
|
| 305 |
+
p[name] = item;
|
| 306 |
+
return p;
|
| 307 |
+
}, Object.assign([], { $: this }));
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
export class EzGraph {
|
| 312 |
+
/** @type { app } */
|
| 313 |
+
app;
|
| 314 |
+
|
| 315 |
+
/**
|
| 316 |
+
* @param { app } app
|
| 317 |
+
*/
|
| 318 |
+
constructor(app) {
|
| 319 |
+
this.app = app;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
get nodes() {
|
| 323 |
+
return this.app.graph._nodes.map((n) => new EzNode(this.app, n));
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
clear() {
|
| 327 |
+
this.app.graph.clear();
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
arrange() {
|
| 331 |
+
this.app.graph.arrange();
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
stringify() {
|
| 335 |
+
return JSON.stringify(this.app.graph.serialize(), undefined);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
/**
|
| 339 |
+
* @param { number | LGNode | EzNode } obj
|
| 340 |
+
* @returns { EzNode }
|
| 341 |
+
*/
|
| 342 |
+
find(obj) {
|
| 343 |
+
let match;
|
| 344 |
+
let id;
|
| 345 |
+
if (typeof obj === "number") {
|
| 346 |
+
id = obj;
|
| 347 |
+
} else {
|
| 348 |
+
id = obj.id;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
match = this.app.graph.getNodeById(id);
|
| 352 |
+
|
| 353 |
+
if (!match) {
|
| 354 |
+
throw new Error(`Unable to find node with ID ${id}.`);
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
return new EzNode(this.app, match);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
/**
|
| 361 |
+
* @returns { Promise<void> }
|
| 362 |
+
*/
|
| 363 |
+
reload() {
|
| 364 |
+
const graph = JSON.parse(JSON.stringify(this.app.graph.serialize()));
|
| 365 |
+
return new Promise((r) => {
|
| 366 |
+
this.app.graph.clear();
|
| 367 |
+
setTimeout(async () => {
|
| 368 |
+
await this.app.loadGraphData(graph);
|
| 369 |
+
r();
|
| 370 |
+
}, 10);
|
| 371 |
+
});
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
/**
|
| 375 |
+
* @returns { Promise<{
|
| 376 |
+
* workflow: {},
|
| 377 |
+
* output: Record<string, {
|
| 378 |
+
* class_name: string,
|
| 379 |
+
* inputs: Record<string, [string, number] | unknown>
|
| 380 |
+
* }>}> }
|
| 381 |
+
*/
|
| 382 |
+
toPrompt() {
|
| 383 |
+
// @ts-ignore
|
| 384 |
+
return this.app.graphToPrompt();
|
| 385 |
+
}
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
export const Ez = {
|
| 389 |
+
/**
|
| 390 |
+
* Quickly build and interact with a ComfyUI graph
|
| 391 |
+
* @example
|
| 392 |
+
* const { ez, graph } = Ez.graph(app);
|
| 393 |
+
* graph.clear();
|
| 394 |
+
* const [model, clip, vae] = ez.CheckpointLoaderSimple().outputs;
|
| 395 |
+
* const [pos] = ez.CLIPTextEncode(clip, { text: "positive" }).outputs;
|
| 396 |
+
* const [neg] = ez.CLIPTextEncode(clip, { text: "negative" }).outputs;
|
| 397 |
+
* const [latent] = ez.KSampler(model, pos, neg, ...ez.EmptyLatentImage().outputs).outputs;
|
| 398 |
+
* const [image] = ez.VAEDecode(latent, vae).outputs;
|
| 399 |
+
* const saveNode = ez.SaveImage(image);
|
| 400 |
+
* console.log(saveNode);
|
| 401 |
+
* graph.arrange();
|
| 402 |
+
* @param { app } app
|
| 403 |
+
* @param { LG["LiteGraph"] } LiteGraph
|
| 404 |
+
* @param { LG["LGraphCanvas"] } LGraphCanvas
|
| 405 |
+
* @param { boolean } clearGraph
|
| 406 |
+
* @returns { { graph: EzGraph, ez: Record<string, EzNodeFactory> } }
|
| 407 |
+
*/
|
| 408 |
+
graph(app, LiteGraph = window["LiteGraph"], LGraphCanvas = window["LGraphCanvas"], clearGraph = true) {
|
| 409 |
+
// Always set the active canvas so things work
|
| 410 |
+
LGraphCanvas.active_canvas = app.canvas;
|
| 411 |
+
|
| 412 |
+
if (clearGraph) {
|
| 413 |
+
app.graph.clear();
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
// @ts-ignore : this proxy handles utility methods & node creation
|
| 417 |
+
const factory = new Proxy(
|
| 418 |
+
{},
|
| 419 |
+
{
|
| 420 |
+
get(_, p) {
|
| 421 |
+
if (typeof p !== "string") throw new Error("Invalid node");
|
| 422 |
+
const node = LiteGraph.createNode(p);
|
| 423 |
+
if (!node) throw new Error(`Unknown node "${p}"`);
|
| 424 |
+
app.graph.add(node);
|
| 425 |
+
|
| 426 |
+
/**
|
| 427 |
+
* @param {Parameters<EzNodeFactory>} args
|
| 428 |
+
*/
|
| 429 |
+
return function (...args) {
|
| 430 |
+
const ezNode = new EzNode(app, node);
|
| 431 |
+
const inputs = ezNode.inputs;
|
| 432 |
+
|
| 433 |
+
let slot = 0;
|
| 434 |
+
for (const arg of args) {
|
| 435 |
+
if (arg instanceof EzOutput) {
|
| 436 |
+
arg.connectTo(inputs[slot++]);
|
| 437 |
+
} else {
|
| 438 |
+
for (const k in arg) {
|
| 439 |
+
ezNode.widgets[k].value = arg[k];
|
| 440 |
+
}
|
| 441 |
+
}
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
return ezNode;
|
| 445 |
+
};
|
| 446 |
+
},
|
| 447 |
+
}
|
| 448 |
+
);
|
| 449 |
+
|
| 450 |
+
return { graph: new EzGraph(app), ez: factory };
|
| 451 |
+
},
|
| 452 |
+
};
|
ComfyUI/tests-ui/utils/index.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { mockApi } = require("./setup");
|
| 2 |
+
const { Ez } = require("./ezgraph");
|
| 3 |
+
const lg = require("./litegraph");
|
| 4 |
+
const fs = require("fs");
|
| 5 |
+
const path = require("path");
|
| 6 |
+
|
| 7 |
+
const html = fs.readFileSync(path.resolve(__dirname, "../../web/index.html"))
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
*
|
| 11 |
+
* @param { Parameters<typeof mockApi>[0] & {
|
| 12 |
+
* resetEnv?: boolean,
|
| 13 |
+
* preSetup?(app): Promise<void>,
|
| 14 |
+
* localStorage?: Record<string, string>
|
| 15 |
+
* } } config
|
| 16 |
+
* @returns
|
| 17 |
+
*/
|
| 18 |
+
export async function start(config = {}) {
|
| 19 |
+
if(config.resetEnv) {
|
| 20 |
+
jest.resetModules();
|
| 21 |
+
jest.resetAllMocks();
|
| 22 |
+
lg.setup(global);
|
| 23 |
+
localStorage.clear();
|
| 24 |
+
sessionStorage.clear();
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
Object.assign(localStorage, config.localStorage ?? {});
|
| 28 |
+
document.body.innerHTML = html;
|
| 29 |
+
|
| 30 |
+
mockApi(config);
|
| 31 |
+
const { app } = require("../../web/scripts/app");
|
| 32 |
+
config.preSetup?.(app);
|
| 33 |
+
await app.setup();
|
| 34 |
+
|
| 35 |
+
return { ...Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]), app };
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* @param { ReturnType<Ez["graph"]>["graph"] } graph
|
| 40 |
+
* @param { (hasReloaded: boolean) => (Promise<void> | void) } cb
|
| 41 |
+
*/
|
| 42 |
+
export async function checkBeforeAndAfterReload(graph, cb) {
|
| 43 |
+
await cb(false);
|
| 44 |
+
await graph.reload();
|
| 45 |
+
await cb(true);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/**
|
| 49 |
+
* @param { string } name
|
| 50 |
+
* @param { Record<string, string | [string | string[], any]> } input
|
| 51 |
+
* @param { (string | string[])[] | Record<string, string | string[]> } output
|
| 52 |
+
* @returns { Record<string, import("../../web/types/comfy").ComfyObjectInfo> }
|
| 53 |
+
*/
|
| 54 |
+
export function makeNodeDef(name, input, output = {}) {
|
| 55 |
+
const nodeDef = {
|
| 56 |
+
name,
|
| 57 |
+
category: "test",
|
| 58 |
+
output: [],
|
| 59 |
+
output_name: [],
|
| 60 |
+
output_is_list: [],
|
| 61 |
+
input: {
|
| 62 |
+
required: {},
|
| 63 |
+
},
|
| 64 |
+
};
|
| 65 |
+
for (const k in input) {
|
| 66 |
+
nodeDef.input.required[k] = typeof input[k] === "string" ? [input[k], {}] : [...input[k]];
|
| 67 |
+
}
|
| 68 |
+
if (output instanceof Array) {
|
| 69 |
+
output = output.reduce((p, c) => {
|
| 70 |
+
p[c] = c;
|
| 71 |
+
return p;
|
| 72 |
+
}, {});
|
| 73 |
+
}
|
| 74 |
+
for (const k in output) {
|
| 75 |
+
nodeDef.output.push(output[k]);
|
| 76 |
+
nodeDef.output_name.push(k);
|
| 77 |
+
nodeDef.output_is_list.push(false);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
return { [name]: nodeDef };
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/**
|
| 84 |
+
/**
|
| 85 |
+
* @template { any } T
|
| 86 |
+
* @param { T } x
|
| 87 |
+
* @returns { x is Exclude<T, null | undefined> }
|
| 88 |
+
*/
|
| 89 |
+
export function assertNotNullOrUndefined(x) {
|
| 90 |
+
expect(x).not.toEqual(null);
|
| 91 |
+
expect(x).not.toEqual(undefined);
|
| 92 |
+
return true;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
*
|
| 97 |
+
* @param { ReturnType<Ez["graph"]>["ez"] } ez
|
| 98 |
+
* @param { ReturnType<Ez["graph"]>["graph"] } graph
|
| 99 |
+
*/
|
| 100 |
+
export function createDefaultWorkflow(ez, graph) {
|
| 101 |
+
graph.clear();
|
| 102 |
+
const ckpt = ez.CheckpointLoaderSimple();
|
| 103 |
+
|
| 104 |
+
const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" });
|
| 105 |
+
const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" });
|
| 106 |
+
|
| 107 |
+
const empty = ez.EmptyLatentImage();
|
| 108 |
+
const sampler = ez.KSampler(
|
| 109 |
+
ckpt.outputs.MODEL,
|
| 110 |
+
pos.outputs.CONDITIONING,
|
| 111 |
+
neg.outputs.CONDITIONING,
|
| 112 |
+
empty.outputs.LATENT
|
| 113 |
+
);
|
| 114 |
+
|
| 115 |
+
const decode = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE);
|
| 116 |
+
const save = ez.SaveImage(decode.outputs.IMAGE);
|
| 117 |
+
graph.arrange();
|
| 118 |
+
|
| 119 |
+
return { ckpt, pos, neg, empty, sampler, decode, save };
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
export async function getNodeDefs() {
|
| 123 |
+
const { api } = require("../../web/scripts/api");
|
| 124 |
+
return api.getNodeDefs();
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
export async function getNodeDef(nodeId) {
|
| 128 |
+
return (await getNodeDefs())[nodeId];
|
| 129 |
+
}
|
ComfyUI/tests-ui/utils/litegraph.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const fs = require("fs");
|
| 2 |
+
const path = require("path");
|
| 3 |
+
const { nop } = require("../utils/nopProxy");
|
| 4 |
+
|
| 5 |
+
function forEachKey(cb) {
|
| 6 |
+
for (const k of [
|
| 7 |
+
"LiteGraph",
|
| 8 |
+
"LGraph",
|
| 9 |
+
"LLink",
|
| 10 |
+
"LGraphNode",
|
| 11 |
+
"LGraphGroup",
|
| 12 |
+
"DragAndScale",
|
| 13 |
+
"LGraphCanvas",
|
| 14 |
+
"ContextMenu",
|
| 15 |
+
]) {
|
| 16 |
+
cb(k);
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export function setup(ctx) {
|
| 21 |
+
const lg = fs.readFileSync(path.resolve("../web/lib/litegraph.core.js"), "utf-8");
|
| 22 |
+
const globalTemp = {};
|
| 23 |
+
(function (console) {
|
| 24 |
+
eval(lg);
|
| 25 |
+
}).call(globalTemp, nop);
|
| 26 |
+
|
| 27 |
+
forEachKey((k) => (ctx[k] = globalTemp[k]));
|
| 28 |
+
require(path.resolve("../web/lib/litegraph.extensions.js"));
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export function teardown(ctx) {
|
| 32 |
+
forEachKey((k) => delete ctx[k]);
|
| 33 |
+
|
| 34 |
+
// Clear document after each run
|
| 35 |
+
document.getElementsByTagName("html")[0].innerHTML = "";
|
| 36 |
+
}
|
ComfyUI/tests-ui/utils/nopProxy.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const nop = new Proxy(function () {}, {
|
| 2 |
+
get: () => nop,
|
| 3 |
+
set: () => true,
|
| 4 |
+
apply: () => nop,
|
| 5 |
+
construct: () => nop,
|
| 6 |
+
});
|
ComfyUI/tests-ui/utils/setup.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
require("../../web/scripts/api");
|
| 2 |
+
|
| 3 |
+
const fs = require("fs");
|
| 4 |
+
const path = require("path");
|
| 5 |
+
function* walkSync(dir) {
|
| 6 |
+
const files = fs.readdirSync(dir, { withFileTypes: true });
|
| 7 |
+
for (const file of files) {
|
| 8 |
+
if (file.isDirectory()) {
|
| 9 |
+
yield* walkSync(path.join(dir, file.name));
|
| 10 |
+
} else {
|
| 11 |
+
yield path.join(dir, file.name);
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* @typedef { import("../../web/types/comfy").ComfyObjectInfo } ComfyObjectInfo
|
| 18 |
+
*/
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* @param {{
|
| 22 |
+
* mockExtensions?: string[],
|
| 23 |
+
* mockNodeDefs?: Record<string, ComfyObjectInfo>,
|
| 24 |
+
* settings?: Record<string, string>
|
| 25 |
+
* userConfig?: {storage: "server" | "browser", users?: Record<string, any>, migrated?: boolean },
|
| 26 |
+
* userData?: Record<string, any>
|
| 27 |
+
* }} config
|
| 28 |
+
*/
|
| 29 |
+
export function mockApi(config = {}) {
|
| 30 |
+
let { mockExtensions, mockNodeDefs, userConfig, settings, userData } = {
|
| 31 |
+
userConfig,
|
| 32 |
+
settings: {},
|
| 33 |
+
userData: {},
|
| 34 |
+
...config,
|
| 35 |
+
};
|
| 36 |
+
if (!mockExtensions) {
|
| 37 |
+
mockExtensions = Array.from(walkSync(path.resolve("../web/extensions/core")))
|
| 38 |
+
.filter((x) => x.endsWith(".js"))
|
| 39 |
+
.map((x) => path.relative(path.resolve("../web"), x));
|
| 40 |
+
}
|
| 41 |
+
if (!mockNodeDefs) {
|
| 42 |
+
mockNodeDefs = JSON.parse(fs.readFileSync(path.resolve("./data/object_info.json")));
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
const events = new EventTarget();
|
| 46 |
+
const mockApi = {
|
| 47 |
+
addEventListener: events.addEventListener.bind(events),
|
| 48 |
+
removeEventListener: events.removeEventListener.bind(events),
|
| 49 |
+
dispatchEvent: events.dispatchEvent.bind(events),
|
| 50 |
+
getSystemStats: jest.fn(),
|
| 51 |
+
getExtensions: jest.fn(() => mockExtensions),
|
| 52 |
+
getNodeDefs: jest.fn(() => mockNodeDefs),
|
| 53 |
+
init: jest.fn(),
|
| 54 |
+
apiURL: jest.fn((x) => "../../web/" + x),
|
| 55 |
+
createUser: jest.fn((username) => {
|
| 56 |
+
if(username in userConfig.users) {
|
| 57 |
+
return { status: 400, json: () => "Duplicate" }
|
| 58 |
+
}
|
| 59 |
+
userConfig.users[username + "!"] = username;
|
| 60 |
+
return { status: 200, json: () => username + "!" }
|
| 61 |
+
}),
|
| 62 |
+
getUserConfig: jest.fn(() => userConfig ?? { storage: "browser", migrated: false }),
|
| 63 |
+
getSettings: jest.fn(() => settings),
|
| 64 |
+
storeSettings: jest.fn((v) => Object.assign(settings, v)),
|
| 65 |
+
getUserData: jest.fn((f) => {
|
| 66 |
+
if (f in userData) {
|
| 67 |
+
return { status: 200, json: () => userData[f] };
|
| 68 |
+
} else {
|
| 69 |
+
return { status: 404 };
|
| 70 |
+
}
|
| 71 |
+
}),
|
| 72 |
+
storeUserData: jest.fn((file, data) => {
|
| 73 |
+
userData[file] = data;
|
| 74 |
+
}),
|
| 75 |
+
};
|
| 76 |
+
jest.mock("../../web/scripts/api", () => ({
|
| 77 |
+
get api() {
|
| 78 |
+
return mockApi;
|
| 79 |
+
},
|
| 80 |
+
}));
|
| 81 |
+
}
|