Remove legacy ui test files (#4316)
This commit is contained in:
parent
f1d6cef71c
commit
ab4dd19b91
|
@ -1,10 +1,4 @@
|
||||||
# This is a temporary action during frontend TS migration.
|
name: Test server launches without errors
|
||||||
# This file should be removed after TS migration is completed.
|
|
||||||
# The browser test is here to ensure TS repo is working the same way as the
|
|
||||||
# current JS code.
|
|
||||||
# If you are adding UI feature, please sync your changes to the TS repo:
|
|
||||||
# huchenlei/ComfyUI_frontend and update test expectation files accordingly.
|
|
||||||
name: Playwright Browser Tests CI
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -21,15 +15,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
repository: "comfyanonymous/ComfyUI"
|
repository: "comfyanonymous/ComfyUI"
|
||||||
path: "ComfyUI"
|
path: "ComfyUI"
|
||||||
- name: Checkout ComfyUI_frontend
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: "huchenlei/ComfyUI_frontend"
|
|
||||||
path: "ComfyUI_frontend"
|
|
||||||
ref: "fcc54d803e5b6a9b08a462a1d94899318c96dcbb"
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: lts/*
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: '3.8'
|
||||||
|
@ -45,16 +30,6 @@ jobs:
|
||||||
python main.py --cpu 2>&1 | tee console_output.log &
|
python main.py --cpu 2>&1 | tee console_output.log &
|
||||||
wait-for-it --service 127.0.0.1:8188 -t 600
|
wait-for-it --service 127.0.0.1:8188 -t 600
|
||||||
working-directory: ComfyUI
|
working-directory: ComfyUI
|
||||||
- name: Install ComfyUI_frontend dependencies
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
working-directory: ComfyUI_frontend
|
|
||||||
- name: Install Playwright Browsers
|
|
||||||
run: npx playwright install --with-deps
|
|
||||||
working-directory: ComfyUI_frontend
|
|
||||||
- name: Run Playwright tests
|
|
||||||
run: npx playwright test
|
|
||||||
working-directory: ComfyUI_frontend
|
|
||||||
- name: Check for unhandled exceptions in server log
|
- name: Check for unhandled exceptions in server log
|
||||||
run: |
|
run: |
|
||||||
if grep -qE "Exception|Error" console_output.log; then
|
if grep -qE "Exception|Error" console_output.log; then
|
||||||
|
@ -62,12 +37,6 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
working-directory: ComfyUI
|
working-directory: ComfyUI
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: playwright-report
|
|
||||||
path: ComfyUI_frontend/playwright-report/
|
|
||||||
retention-days: 30
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
|
@ -1,30 +0,0 @@
|
||||||
name: Tests CI
|
|
||||||
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: '3.10'
|
|
||||||
- name: Install requirements
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
|
||||||
pip install -r requirements.txt
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
npm run test:generate
|
|
||||||
npm test -- --verbose
|
|
||||||
working-directory: ./tests-ui
|
|
||||||
- name: Run Unit Tests
|
|
||||||
run: |
|
|
||||||
pip install -r tests-unit/requirements.txt
|
|
||||||
python -m pytest tests-unit
|
|
|
@ -1 +0,0 @@
|
||||||
node_modules
|
|
|
@ -1,9 +0,0 @@
|
||||||
const { start } = require("./utils");
|
|
||||||
const lg = require("./utils/litegraph");
|
|
||||||
|
|
||||||
// Load things once per test file before to ensure its all warmed up for the tests
|
|
||||||
beforeAll(async () => {
|
|
||||||
lg.setup(global);
|
|
||||||
await start({ resetEnv: true });
|
|
||||||
lg.teardown(global);
|
|
||||||
});
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"presets": ["@babel/preset-env"],
|
|
||||||
"plugins": ["babel-plugin-transform-import-meta"]
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
module.exports = async function () {
|
|
||||||
global.ResizeObserver = class ResizeObserver {
|
|
||||||
observe() {}
|
|
||||||
unobserve() {}
|
|
||||||
disconnect() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { nop } = require("./utils/nopProxy");
|
|
||||||
global.enableWebGLCanvas = nop;
|
|
||||||
|
|
||||||
HTMLCanvasElement.prototype.getContext = nop;
|
|
||||||
|
|
||||||
localStorage["Comfy.Settings.Comfy.Logging.Enabled"] = "false";
|
|
||||||
};
|
|
|
@ -1,11 +0,0 @@
|
||||||
/** @type {import('jest').Config} */
|
|
||||||
const config = {
|
|
||||||
testEnvironment: "jsdom",
|
|
||||||
setupFiles: ["./globalSetup.js"],
|
|
||||||
setupFilesAfterEnv: ["./afterSetup.js"],
|
|
||||||
clearMocks: true,
|
|
||||||
resetModules: true,
|
|
||||||
testTimeout: 10000
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = config;
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"name": "comfui-tests",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "UI tests",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "jest",
|
|
||||||
"test:generate": "node setup.js"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/comfyanonymous/ComfyUI.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"comfyui",
|
|
||||||
"test"
|
|
||||||
],
|
|
||||||
"author": "comfyanonymous",
|
|
||||||
"license": "GPL-3.0",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/comfyanonymous/ComfyUI/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/comfyanonymous/ComfyUI#readme",
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/preset-env": "^7.22.20",
|
|
||||||
"@types/jest": "^29.5.5",
|
|
||||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"jest-environment-jsdom": "^29.7.0"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
const { spawn } = require("child_process");
|
|
||||||
const { resolve } = require("path");
|
|
||||||
const { existsSync, mkdirSync, writeFileSync } = require("fs");
|
|
||||||
const http = require("http");
|
|
||||||
|
|
||||||
async function setup() {
|
|
||||||
// Wait up to 30s for it to start
|
|
||||||
let success = false;
|
|
||||||
let child;
|
|
||||||
for (let i = 0; i < 30; i++) {
|
|
||||||
try {
|
|
||||||
await new Promise((res, rej) => {
|
|
||||||
http
|
|
||||||
.get("http://127.0.0.1:8188/object_info", (resp) => {
|
|
||||||
let data = "";
|
|
||||||
resp.on("data", (chunk) => {
|
|
||||||
data += chunk;
|
|
||||||
});
|
|
||||||
resp.on("end", () => {
|
|
||||||
// Modify the response data to add some checkpoints
|
|
||||||
const objectInfo = JSON.parse(data);
|
|
||||||
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name[0] = ["model1.safetensors", "model2.ckpt"];
|
|
||||||
objectInfo.VAELoader.input.required.vae_name[0] = ["vae1.safetensors", "vae2.ckpt"];
|
|
||||||
|
|
||||||
data = JSON.stringify(objectInfo, undefined, "\t");
|
|
||||||
|
|
||||||
const outDir = resolve("./data");
|
|
||||||
if (!existsSync(outDir)) {
|
|
||||||
mkdirSync(outDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
const outPath = resolve(outDir, "object_info.json");
|
|
||||||
console.log(`Writing ${Object.keys(objectInfo).length} nodes to ${outPath}`);
|
|
||||||
writeFileSync(outPath, data, {
|
|
||||||
encoding: "utf8",
|
|
||||||
});
|
|
||||||
res();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.on("error", rej);
|
|
||||||
});
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(i + "/30", error);
|
|
||||||
if (i === 0) {
|
|
||||||
// Start the server on first iteration if it fails to connect
|
|
||||||
console.log("Starting ComfyUI server...");
|
|
||||||
|
|
||||||
let python = resolve("../../python_embeded/python.exe");
|
|
||||||
let args;
|
|
||||||
let cwd;
|
|
||||||
if (existsSync(python)) {
|
|
||||||
args = ["-s", "ComfyUI/main.py"];
|
|
||||||
cwd = "../..";
|
|
||||||
} else {
|
|
||||||
python = "python";
|
|
||||||
args = ["main.py"];
|
|
||||||
cwd = "..";
|
|
||||||
}
|
|
||||||
args.push("--cpu");
|
|
||||||
console.log(python, ...args);
|
|
||||||
child = spawn(python, args, { cwd });
|
|
||||||
child.on("error", (err) => {
|
|
||||||
console.log(`Server error (${err})`);
|
|
||||||
i = 30;
|
|
||||||
});
|
|
||||||
child.on("exit", (code) => {
|
|
||||||
if (!success) {
|
|
||||||
console.log(`Server exited (${code})`);
|
|
||||||
i = 30;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await new Promise((r) => {
|
|
||||||
setTimeout(r, 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
child?.kill();
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
throw new Error("Waiting for server failed...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setup();
|
|
|
@ -1,196 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
/// <reference path="../node_modules/@types/jest/index.d.ts" />
|
|
||||||
const { start } = require("../utils");
|
|
||||||
const lg = require("../utils/litegraph");
|
|
||||||
|
|
||||||
describe("extensions", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
lg.setup(global);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
lg.teardown(global);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls each extension hook", async () => {
|
|
||||||
const mockExtension = {
|
|
||||||
name: "TestExtension",
|
|
||||||
init: jest.fn(),
|
|
||||||
setup: jest.fn(),
|
|
||||||
addCustomNodeDefs: jest.fn(),
|
|
||||||
getCustomWidgets: jest.fn(),
|
|
||||||
beforeRegisterNodeDef: jest.fn(),
|
|
||||||
registerCustomNodes: jest.fn(),
|
|
||||||
loadedGraphNode: jest.fn(),
|
|
||||||
nodeCreated: jest.fn(),
|
|
||||||
beforeConfigureGraph: jest.fn(),
|
|
||||||
afterConfigureGraph: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const { app, ez, graph } = await start({
|
|
||||||
async preSetup(app) {
|
|
||||||
app.registerExtension(mockExtension);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Basic initialisation hooks should be called once, with app
|
|
||||||
expect(mockExtension.init).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockExtension.init).toHaveBeenCalledWith(app);
|
|
||||||
|
|
||||||
// Adding custom node defs should be passed the full list of nodes
|
|
||||||
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockExtension.addCustomNodeDefs.mock.calls[0][1]).toStrictEqual(app);
|
|
||||||
const defs = mockExtension.addCustomNodeDefs.mock.calls[0][0];
|
|
||||||
expect(defs).toHaveProperty("KSampler");
|
|
||||||
expect(defs).toHaveProperty("LoadImage");
|
|
||||||
|
|
||||||
// Get custom widgets is called once and should return new widget types
|
|
||||||
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockExtension.getCustomWidgets).toHaveBeenCalledWith(app);
|
|
||||||
|
|
||||||
// Before register node def will be called once per node type
|
|
||||||
const nodeNames = Object.keys(defs);
|
|
||||||
const nodeCount = nodeNames.length;
|
|
||||||
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount);
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
// It should be send the JS class and the original JSON definition
|
|
||||||
const nodeClass = mockExtension.beforeRegisterNodeDef.mock.calls[i][0];
|
|
||||||
const nodeDef = mockExtension.beforeRegisterNodeDef.mock.calls[i][1];
|
|
||||||
|
|
||||||
expect(nodeClass.name).toBe("ComfyNode");
|
|
||||||
expect(nodeClass.comfyClass).toBe(nodeNames[i]);
|
|
||||||
expect(nodeDef.name).toBe(nodeNames[i]);
|
|
||||||
expect(nodeDef).toHaveProperty("input");
|
|
||||||
expect(nodeDef).toHaveProperty("output");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register custom nodes is called once after registerNode defs to allow adding other frontend nodes
|
|
||||||
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
// Before configure graph will be called here as the default graph is being loaded
|
|
||||||
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(1);
|
|
||||||
// it gets sent the graph data that is going to be loaded
|
|
||||||
const graphData = mockExtension.beforeConfigureGraph.mock.calls[0][0];
|
|
||||||
|
|
||||||
// A node created is fired for each node constructor that is called
|
|
||||||
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length);
|
|
||||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
|
||||||
expect(mockExtension.nodeCreated.mock.calls[i][0].type).toBe(graphData.nodes[i].type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Each node then calls loadedGraphNode to allow them to be updated
|
|
||||||
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length);
|
|
||||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
|
||||||
expect(mockExtension.loadedGraphNode.mock.calls[i][0].type).toBe(graphData.nodes[i].type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// After configure is then called once all the setup is done
|
|
||||||
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(mockExtension.setup).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockExtension.setup).toHaveBeenCalledWith(app);
|
|
||||||
|
|
||||||
// Ensure hooks are called in the correct order
|
|
||||||
const callOrder = [
|
|
||||||
"init",
|
|
||||||
"addCustomNodeDefs",
|
|
||||||
"getCustomWidgets",
|
|
||||||
"beforeRegisterNodeDef",
|
|
||||||
"registerCustomNodes",
|
|
||||||
"beforeConfigureGraph",
|
|
||||||
"nodeCreated",
|
|
||||||
"loadedGraphNode",
|
|
||||||
"afterConfigureGraph",
|
|
||||||
"setup",
|
|
||||||
];
|
|
||||||
for (let i = 1; i < callOrder.length; i++) {
|
|
||||||
const fn1 = mockExtension[callOrder[i - 1]];
|
|
||||||
const fn2 = mockExtension[callOrder[i]];
|
|
||||||
expect(fn1.mock.invocationCallOrder[0]).toBeLessThan(fn2.mock.invocationCallOrder[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
graph.clear();
|
|
||||||
|
|
||||||
// Ensure adding a new node calls the correct callback
|
|
||||||
ez.LoadImage();
|
|
||||||
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length);
|
|
||||||
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 1);
|
|
||||||
expect(mockExtension.nodeCreated.mock.lastCall[0].type).toBe("LoadImage");
|
|
||||||
|
|
||||||
// Reload the graph to ensure correct hooks are fired
|
|
||||||
await graph.reload();
|
|
||||||
|
|
||||||
// These hooks should not be fired again
|
|
||||||
expect(mockExtension.init).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount);
|
|
||||||
expect(mockExtension.setup).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
// These should be called again
|
|
||||||
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(2);
|
|
||||||
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 2);
|
|
||||||
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length + 1);
|
|
||||||
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(2);
|
|
||||||
}, 15000);
|
|
||||||
|
|
||||||
it("allows custom nodeDefs and widgets to be registered", async () => {
|
|
||||||
const widgetMock = jest.fn((node, inputName, inputData, app) => {
|
|
||||||
expect(node.constructor.comfyClass).toBe("TestNode");
|
|
||||||
expect(inputName).toBe("test_input");
|
|
||||||
expect(inputData[0]).toBe("CUSTOMWIDGET");
|
|
||||||
expect(inputData[1]?.hello).toBe("world");
|
|
||||||
expect(app).toStrictEqual(app);
|
|
||||||
|
|
||||||
return {
|
|
||||||
widget: node.addWidget("button", inputName, "hello", () => {}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register our extension that adds a custom node + widget type
|
|
||||||
const mockExtension = {
|
|
||||||
name: "TestExtension",
|
|
||||||
addCustomNodeDefs: (nodeDefs) => {
|
|
||||||
nodeDefs["TestNode"] = {
|
|
||||||
output: [],
|
|
||||||
output_name: [],
|
|
||||||
output_is_list: [],
|
|
||||||
name: "TestNode",
|
|
||||||
display_name: "TestNode",
|
|
||||||
category: "Test",
|
|
||||||
input: {
|
|
||||||
required: {
|
|
||||||
test_input: ["CUSTOMWIDGET", { hello: "world" }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getCustomWidgets: jest.fn(() => {
|
|
||||||
return {
|
|
||||||
CUSTOMWIDGET: widgetMock,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const { graph, ez } = await start({
|
|
||||||
async preSetup(app) {
|
|
||||||
app.registerExtension(mockExtension);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockExtension.getCustomWidgets).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
graph.clear();
|
|
||||||
expect(widgetMock).toBeCalledTimes(0);
|
|
||||||
const node = ez.TestNode();
|
|
||||||
expect(widgetMock).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
// Ensure our custom widget is created
|
|
||||||
expect(node.inputs.length).toBe(0);
|
|
||||||
expect(node.widgets.length).toBe(1);
|
|
||||||
const w = node.widgets[0].widget;
|
|
||||||
expect(w.name).toBe("test_input");
|
|
||||||
expect(w.type).toBe("button");
|
|
||||||
});
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,295 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
/// <reference path="../node_modules/@types/jest/index.d.ts" />
|
|
||||||
const { start } = require("../utils");
|
|
||||||
const lg = require("../utils/litegraph");
|
|
||||||
|
|
||||||
describe("users", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
lg.setup(global);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
lg.teardown(global);
|
|
||||||
});
|
|
||||||
|
|
||||||
function expectNoUserScreen() {
|
|
||||||
// Ensure login isnt visible
|
|
||||||
const selection = document.querySelectorAll("#comfy-user-selection")?.[0];
|
|
||||||
expect(selection["style"].display).toBe("none");
|
|
||||||
const menu = document.querySelectorAll(".comfy-menu")?.[0];
|
|
||||||
expect(window.getComputedStyle(menu)?.display).not.toBe("none");
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("multi-user", () => {
|
|
||||||
function mockAddStylesheet() {
|
|
||||||
const utils = require("../../web/scripts/utils");
|
|
||||||
utils.addStylesheet = jest.fn().mockReturnValue(Promise.resolve());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForUserScreenShow() {
|
|
||||||
mockAddStylesheet();
|
|
||||||
|
|
||||||
// Wait for "show" to be called
|
|
||||||
const { UserSelectionScreen } = require("../../web/scripts/ui/userSelection");
|
|
||||||
let resolve, reject;
|
|
||||||
const fn = UserSelectionScreen.prototype.show;
|
|
||||||
const p = new Promise((res, rej) => {
|
|
||||||
resolve = res;
|
|
||||||
reject = rej;
|
|
||||||
});
|
|
||||||
jest.spyOn(UserSelectionScreen.prototype, "show").mockImplementation(async (...args) => {
|
|
||||||
const res = fn(...args);
|
|
||||||
await new Promise(process.nextTick); // wait for promises to resolve
|
|
||||||
resolve();
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
// @ts-ignore
|
|
||||||
setTimeout(() => reject("timeout waiting for UserSelectionScreen to be shown."), 500);
|
|
||||||
await p;
|
|
||||||
await new Promise(process.nextTick); // wait for promises to resolve
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testUserScreen(onShown, users) {
|
|
||||||
if (!users) {
|
|
||||||
users = {};
|
|
||||||
}
|
|
||||||
const starting = start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { storage: "server", users },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure no current user
|
|
||||||
expect(localStorage["Comfy.userId"]).toBeFalsy();
|
|
||||||
expect(localStorage["Comfy.userName"]).toBeFalsy();
|
|
||||||
|
|
||||||
await waitForUserScreenShow();
|
|
||||||
|
|
||||||
const selection = document.querySelectorAll("#comfy-user-selection")?.[0];
|
|
||||||
expect(selection).toBeTruthy();
|
|
||||||
|
|
||||||
// Ensure login is visible
|
|
||||||
expect(window.getComputedStyle(selection)?.display).not.toBe("none");
|
|
||||||
// Ensure menu is hidden
|
|
||||||
const menu = document.querySelectorAll(".comfy-menu")?.[0];
|
|
||||||
expect(window.getComputedStyle(menu)?.display).toBe("none");
|
|
||||||
|
|
||||||
const isCreate = await onShown(selection);
|
|
||||||
|
|
||||||
// Submit form
|
|
||||||
selection.querySelectorAll("form")[0].submit();
|
|
||||||
await new Promise(process.nextTick); // wait for promises to resolve
|
|
||||||
|
|
||||||
// Wait for start
|
|
||||||
const s = await starting;
|
|
||||||
|
|
||||||
// Ensure login is removed
|
|
||||||
expect(document.querySelectorAll("#comfy-user-selection")).toHaveLength(0);
|
|
||||||
expect(window.getComputedStyle(menu)?.display).not.toBe("none");
|
|
||||||
|
|
||||||
// Ensure settings + templates are saved
|
|
||||||
const { api } = require("../../web/scripts/api");
|
|
||||||
expect(api.createUser).toHaveBeenCalledTimes(+isCreate);
|
|
||||||
expect(api.storeSettings).toHaveBeenCalledTimes(+isCreate);
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledTimes(+isCreate);
|
|
||||||
if (isCreate) {
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false });
|
|
||||||
expect(s.app.isNewUserSession).toBeTruthy();
|
|
||||||
} else {
|
|
||||||
expect(s.app.isNewUserSession).toBeFalsy();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { users, selection, ...s };
|
|
||||||
}
|
|
||||||
|
|
||||||
it("allows user creation if no users", async () => {
|
|
||||||
const { users } = await testUserScreen((selection) => {
|
|
||||||
// Ensure we have no users flag added
|
|
||||||
expect(selection.classList.contains("no-users")).toBeTruthy();
|
|
||||||
|
|
||||||
// Enter a username
|
|
||||||
const input = selection.getElementsByTagName("input")[0];
|
|
||||||
input.focus();
|
|
||||||
input.value = "Test User";
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(users).toStrictEqual({
|
|
||||||
"Test User!": "Test User",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(localStorage["Comfy.userId"]).toBe("Test User!");
|
|
||||||
expect(localStorage["Comfy.userName"]).toBe("Test User");
|
|
||||||
});
|
|
||||||
it("allows user creation if no current user but other users", async () => {
|
|
||||||
const users = {
|
|
||||||
"Test User 2!": "Test User 2",
|
|
||||||
};
|
|
||||||
|
|
||||||
await testUserScreen((selection) => {
|
|
||||||
expect(selection.classList.contains("no-users")).toBeFalsy();
|
|
||||||
|
|
||||||
// Enter a username
|
|
||||||
const input = selection.getElementsByTagName("input")[0];
|
|
||||||
input.focus();
|
|
||||||
input.value = "Test User 3";
|
|
||||||
return true;
|
|
||||||
}, users);
|
|
||||||
|
|
||||||
expect(users).toStrictEqual({
|
|
||||||
"Test User 2!": "Test User 2",
|
|
||||||
"Test User 3!": "Test User 3",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(localStorage["Comfy.userId"]).toBe("Test User 3!");
|
|
||||||
expect(localStorage["Comfy.userName"]).toBe("Test User 3");
|
|
||||||
});
|
|
||||||
it("allows user selection if no current user but other users", async () => {
|
|
||||||
const users = {
|
|
||||||
"A!": "A",
|
|
||||||
"B!": "B",
|
|
||||||
"C!": "C",
|
|
||||||
};
|
|
||||||
|
|
||||||
await testUserScreen((selection) => {
|
|
||||||
expect(selection.classList.contains("no-users")).toBeFalsy();
|
|
||||||
|
|
||||||
// Check user list
|
|
||||||
const select = selection.getElementsByTagName("select")[0];
|
|
||||||
const options = select.getElementsByTagName("option");
|
|
||||||
expect(
|
|
||||||
[...options]
|
|
||||||
.filter((o) => !o.disabled)
|
|
||||||
.reduce((p, n) => {
|
|
||||||
p[n.getAttribute("value")] = n.textContent;
|
|
||||||
return p;
|
|
||||||
}, {})
|
|
||||||
).toStrictEqual(users);
|
|
||||||
|
|
||||||
// Select an option
|
|
||||||
select.focus();
|
|
||||||
select.value = options[2].value;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}, users);
|
|
||||||
|
|
||||||
expect(users).toStrictEqual(users);
|
|
||||||
|
|
||||||
expect(localStorage["Comfy.userId"]).toBe("B!");
|
|
||||||
expect(localStorage["Comfy.userName"]).toBe("B");
|
|
||||||
});
|
|
||||||
it("doesnt show user screen if current user", async () => {
|
|
||||||
const starting = start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: {
|
|
||||||
storage: "server",
|
|
||||||
users: {
|
|
||||||
"User!": "User",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
localStorage: {
|
|
||||||
"Comfy.userId": "User!",
|
|
||||||
"Comfy.userName": "User",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await new Promise(process.nextTick); // wait for promises to resolve
|
|
||||||
|
|
||||||
expectNoUserScreen();
|
|
||||||
|
|
||||||
await starting;
|
|
||||||
});
|
|
||||||
it("allows user switching", async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: {
|
|
||||||
storage: "server",
|
|
||||||
users: {
|
|
||||||
"User!": "User",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
localStorage: {
|
|
||||||
"Comfy.userId": "User!",
|
|
||||||
"Comfy.userName": "User",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// cant actually test switching user easily but can check the setting is present
|
|
||||||
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("single-user", () => {
|
|
||||||
it("doesnt show user creation if no default user", async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: false, storage: "server" },
|
|
||||||
});
|
|
||||||
expectNoUserScreen();
|
|
||||||
|
|
||||||
// It should store the settings
|
|
||||||
const { api } = require("../../web/scripts/api");
|
|
||||||
expect(api.storeSettings).toHaveBeenCalledTimes(1);
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledTimes(1);
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false });
|
|
||||||
expect(app.isNewUserSession).toBeTruthy();
|
|
||||||
});
|
|
||||||
it("doesnt show user creation if default user", async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: true, storage: "server" },
|
|
||||||
});
|
|
||||||
expectNoUserScreen();
|
|
||||||
|
|
||||||
// It should store the settings
|
|
||||||
const { api } = require("../../web/scripts/api");
|
|
||||||
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
|
||||||
expect(app.isNewUserSession).toBeFalsy();
|
|
||||||
});
|
|
||||||
it("doesnt allow user switching", async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: true, storage: "server" },
|
|
||||||
});
|
|
||||||
expectNoUserScreen();
|
|
||||||
|
|
||||||
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("browser-user", () => {
|
|
||||||
it("doesnt show user creation if no default user", async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: false, storage: "browser" },
|
|
||||||
});
|
|
||||||
expectNoUserScreen();
|
|
||||||
|
|
||||||
// It should store the settings
|
|
||||||
const { api } = require("../../web/scripts/api");
|
|
||||||
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
|
||||||
expect(app.isNewUserSession).toBeFalsy();
|
|
||||||
});
|
|
||||||
it("doesnt show user creation if default user", async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: true, storage: "server" },
|
|
||||||
});
|
|
||||||
expectNoUserScreen();
|
|
||||||
|
|
||||||
// It should store the settings
|
|
||||||
const { api } = require("../../web/scripts/api");
|
|
||||||
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
|
||||||
expect(app.isNewUserSession).toBeFalsy();
|
|
||||||
});
|
|
||||||
it("doesnt allow user switching", async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: true, storage: "browser" },
|
|
||||||
});
|
|
||||||
expectNoUserScreen();
|
|
||||||
|
|
||||||
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,557 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
/// <reference path="../node_modules/@types/jest/index.d.ts" />
|
|
||||||
|
|
||||||
const {
|
|
||||||
start,
|
|
||||||
makeNodeDef,
|
|
||||||
checkBeforeAndAfterReload,
|
|
||||||
assertNotNullOrUndefined,
|
|
||||||
createDefaultWorkflow,
|
|
||||||
} = require("../utils");
|
|
||||||
const lg = require("../utils/litegraph");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef { import("../utils/ezgraph") } Ez
|
|
||||||
* @typedef { ReturnType<Ez["Ez"]["graph"]>["ez"] } EzNodeFactory
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { EzNodeFactory } ez
|
|
||||||
* @param { InstanceType<Ez["EzGraph"]> } graph
|
|
||||||
* @param { InstanceType<Ez["EzInput"]> } input
|
|
||||||
* @param { string } widgetType
|
|
||||||
* @param { number } controlWidgetCount
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async function connectPrimitiveAndReload(ez, graph, input, widgetType, controlWidgetCount = 0) {
|
|
||||||
// Connect to primitive and ensure its still connected after
|
|
||||||
let primitive = ez.PrimitiveNode();
|
|
||||||
primitive.outputs[0].connectTo(input);
|
|
||||||
|
|
||||||
await checkBeforeAndAfterReload(graph, async () => {
|
|
||||||
primitive = graph.find(primitive);
|
|
||||||
let { connections } = primitive.outputs[0];
|
|
||||||
expect(connections).toHaveLength(1);
|
|
||||||
expect(connections[0].targetNode.id).toBe(input.node.node.id);
|
|
||||||
|
|
||||||
// Ensure widget is correct type
|
|
||||||
const valueWidget = primitive.widgets.value;
|
|
||||||
expect(valueWidget.widget.type).toBe(widgetType);
|
|
||||||
|
|
||||||
// Check if control_after_generate should be added
|
|
||||||
if (controlWidgetCount) {
|
|
||||||
const controlWidget = primitive.widgets.control_after_generate;
|
|
||||||
expect(controlWidget.widget.type).toBe("combo");
|
|
||||||
if (widgetType === "combo") {
|
|
||||||
const filterWidget = primitive.widgets.control_filter_list;
|
|
||||||
expect(filterWidget.widget.type).toBe("string");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we dont have other widgets
|
|
||||||
expect(primitive.node.widgets).toHaveLength(1 + controlWidgetCount);
|
|
||||||
});
|
|
||||||
|
|
||||||
return primitive;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("widget inputs", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
lg.setup(global);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
lg.teardown(global);
|
|
||||||
});
|
|
||||||
|
|
||||||
[
|
|
||||||
{ name: "int", type: "INT", widget: "number", control: 1 },
|
|
||||||
{ name: "float", type: "FLOAT", widget: "number", control: 1 },
|
|
||||||
{ name: "text", type: "STRING" },
|
|
||||||
{
|
|
||||||
name: "customtext",
|
|
||||||
type: "STRING",
|
|
||||||
opt: { multiline: true },
|
|
||||||
},
|
|
||||||
{ name: "toggle", type: "BOOLEAN" },
|
|
||||||
{ name: "combo", type: ["a", "b", "c"], control: 2 },
|
|
||||||
].forEach((c) => {
|
|
||||||
test(`widget conversion + primitive works on ${c.name}`, async () => {
|
|
||||||
const { ez, graph } = await start({
|
|
||||||
mockNodeDefs: makeNodeDef("TestNode", { [c.name]: [c.type, c.opt ?? {}] }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create test node and convert to input
|
|
||||||
const n = ez.TestNode();
|
|
||||||
const w = n.widgets[c.name];
|
|
||||||
w.convertToInput();
|
|
||||||
expect(w.isConvertedToInput).toBeTruthy();
|
|
||||||
const input = w.getConvertedInput();
|
|
||||||
expect(input).toBeTruthy();
|
|
||||||
|
|
||||||
// @ts-ignore : input is valid here
|
|
||||||
await connectPrimitiveAndReload(ez, graph, input, c.widget ?? c.name, c.control);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("converted widget works after reload", async () => {
|
|
||||||
const { ez, graph } = await start();
|
|
||||||
let n = ez.CheckpointLoaderSimple();
|
|
||||||
|
|
||||||
const inputCount = n.inputs.length;
|
|
||||||
|
|
||||||
// Convert ckpt name to an input
|
|
||||||
n.widgets.ckpt_name.convertToInput();
|
|
||||||
expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
|
|
||||||
expect(n.inputs.ckpt_name).toBeTruthy();
|
|
||||||
expect(n.inputs.length).toEqual(inputCount + 1);
|
|
||||||
|
|
||||||
// Convert back to widget and ensure input is removed
|
|
||||||
n.widgets.ckpt_name.convertToWidget();
|
|
||||||
expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
|
|
||||||
expect(n.inputs.ckpt_name).toBeFalsy();
|
|
||||||
expect(n.inputs.length).toEqual(inputCount);
|
|
||||||
|
|
||||||
// Convert again and reload the graph to ensure it maintains state
|
|
||||||
n.widgets.ckpt_name.convertToInput();
|
|
||||||
expect(n.inputs.length).toEqual(inputCount + 1);
|
|
||||||
|
|
||||||
const primitive = await connectPrimitiveAndReload(ez, graph, n.inputs.ckpt_name, "combo", 2);
|
|
||||||
|
|
||||||
// Disconnect & reconnect
|
|
||||||
primitive.outputs[0].connections[0].disconnect();
|
|
||||||
let { connections } = primitive.outputs[0];
|
|
||||||
expect(connections).toHaveLength(0);
|
|
||||||
|
|
||||||
primitive.outputs[0].connectTo(n.inputs.ckpt_name);
|
|
||||||
({ connections } = primitive.outputs[0]);
|
|
||||||
expect(connections).toHaveLength(1);
|
|
||||||
expect(connections[0].targetNode.id).toBe(n.node.id);
|
|
||||||
|
|
||||||
// Convert back to widget and ensure input is removed
|
|
||||||
n.widgets.ckpt_name.convertToWidget();
|
|
||||||
expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
|
|
||||||
expect(n.inputs.ckpt_name).toBeFalsy();
|
|
||||||
expect(n.inputs.length).toEqual(inputCount);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("converted widget works on clone", async () => {
|
|
||||||
const { graph, ez } = await start();
|
|
||||||
let n = ez.CheckpointLoaderSimple();
|
|
||||||
|
|
||||||
// Convert the widget to an input
|
|
||||||
n.widgets.ckpt_name.convertToInput();
|
|
||||||
expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
|
|
||||||
|
|
||||||
// Clone the node
|
|
||||||
n.menu["Clone"].call();
|
|
||||||
expect(graph.nodes).toHaveLength(2);
|
|
||||||
const clone = graph.nodes[1];
|
|
||||||
expect(clone.id).not.toEqual(n.id);
|
|
||||||
|
|
||||||
// Ensure the clone has an input
|
|
||||||
expect(clone.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
|
|
||||||
expect(clone.inputs.ckpt_name).toBeTruthy();
|
|
||||||
|
|
||||||
// Ensure primitive connects to both nodes
|
|
||||||
let primitive = ez.PrimitiveNode();
|
|
||||||
primitive.outputs[0].connectTo(n.inputs.ckpt_name);
|
|
||||||
primitive.outputs[0].connectTo(clone.inputs.ckpt_name);
|
|
||||||
expect(primitive.outputs[0].connections).toHaveLength(2);
|
|
||||||
|
|
||||||
// Convert back to widget and ensure input is removed
|
|
||||||
clone.widgets.ckpt_name.convertToWidget();
|
|
||||||
expect(clone.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
|
|
||||||
expect(clone.inputs.ckpt_name).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("shows missing node error on custom node with converted input", async () => {
|
|
||||||
const { graph } = await start();
|
|
||||||
|
|
||||||
const dialogShow = jest.spyOn(graph.app.ui.dialog, "show");
|
|
||||||
|
|
||||||
await graph.app.loadGraphData({
|
|
||||||
last_node_id: 3,
|
|
||||||
last_link_id: 4,
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
type: "TestNode",
|
|
||||||
pos: [41.87329101561909, 389.7381480823742],
|
|
||||||
size: { 0: 220, 1: 374 },
|
|
||||||
flags: {},
|
|
||||||
order: 1,
|
|
||||||
mode: 0,
|
|
||||||
inputs: [{ name: "test", type: "FLOAT", link: 4, widget: { name: "test" }, slot_index: 0 }],
|
|
||||||
outputs: [],
|
|
||||||
properties: { "Node name for S&R": "TestNode" },
|
|
||||||
widgets_values: [1],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
type: "PrimitiveNode",
|
|
||||||
pos: [-312, 433],
|
|
||||||
size: { 0: 210, 1: 82 },
|
|
||||||
flags: {},
|
|
||||||
order: 0,
|
|
||||||
mode: 0,
|
|
||||||
outputs: [{ links: [4], widget: { name: "test" } }],
|
|
||||||
title: "test",
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
links: [[4, 3, 0, 1, 6, "FLOAT"]],
|
|
||||||
groups: [],
|
|
||||||
config: {},
|
|
||||||
extra: {},
|
|
||||||
version: 0.4,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(dialogShow).toBeCalledTimes(1);
|
|
||||||
expect(dialogShow.mock.calls[0][0].innerHTML).toContain("the following node types were not found");
|
|
||||||
expect(dialogShow.mock.calls[0][0].innerHTML).toContain("TestNode");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("defaultInput widgets can be converted back to inputs", async () => {
|
|
||||||
const { graph, ez } = await start({
|
|
||||||
mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { defaultInput: true }] }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create test node and ensure it starts as an input
|
|
||||||
let n = ez.TestNode();
|
|
||||||
let w = n.widgets.example;
|
|
||||||
expect(w.isConvertedToInput).toBeTruthy();
|
|
||||||
let input = w.getConvertedInput();
|
|
||||||
expect(input).toBeTruthy();
|
|
||||||
|
|
||||||
// Ensure it can be converted to
|
|
||||||
w.convertToWidget();
|
|
||||||
expect(w.isConvertedToInput).toBeFalsy();
|
|
||||||
expect(n.inputs.length).toEqual(0);
|
|
||||||
// and from
|
|
||||||
w.convertToInput();
|
|
||||||
expect(w.isConvertedToInput).toBeTruthy();
|
|
||||||
input = w.getConvertedInput();
|
|
||||||
|
|
||||||
// Reload and ensure it still only has 1 converted widget
|
|
||||||
if (!assertNotNullOrUndefined(input)) return;
|
|
||||||
|
|
||||||
await connectPrimitiveAndReload(ez, graph, input, "number", 1);
|
|
||||||
n = graph.find(n);
|
|
||||||
expect(n.widgets).toHaveLength(1);
|
|
||||||
w = n.widgets.example;
|
|
||||||
expect(w.isConvertedToInput).toBeTruthy();
|
|
||||||
|
|
||||||
// Convert back to widget and ensure it is still a widget after reload
|
|
||||||
w.convertToWidget();
|
|
||||||
await graph.reload();
|
|
||||||
n = graph.find(n);
|
|
||||||
expect(n.widgets).toHaveLength(1);
|
|
||||||
expect(n.widgets[0].isConvertedToInput).toBeFalsy();
|
|
||||||
expect(n.inputs.length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("forceInput widgets can not be converted back to inputs", async () => {
|
|
||||||
const { graph, ez } = await start({
|
|
||||||
mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { forceInput: true }] }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create test node and ensure it starts as an input
|
|
||||||
let n = ez.TestNode();
|
|
||||||
let w = n.widgets.example;
|
|
||||||
expect(w.isConvertedToInput).toBeTruthy();
|
|
||||||
const input = w.getConvertedInput();
|
|
||||||
expect(input).toBeTruthy();
|
|
||||||
|
|
||||||
// Convert to widget should error
|
|
||||||
expect(() => w.convertToWidget()).toThrow();
|
|
||||||
|
|
||||||
// Reload and ensure it still only has 1 converted widget
|
|
||||||
if (assertNotNullOrUndefined(input)) {
|
|
||||||
await connectPrimitiveAndReload(ez, graph, input, "number", 1);
|
|
||||||
n = graph.find(n);
|
|
||||||
expect(n.widgets).toHaveLength(1);
|
|
||||||
expect(n.widgets.example.isConvertedToInput).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("primitive can connect to matching combos on converted widgets", async () => {
|
|
||||||
const { ez } = await start({
|
|
||||||
mockNodeDefs: {
|
|
||||||
...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }),
|
|
||||||
...makeNodeDef("TestNode2", { example: [["A", "B", "C"], { forceInput: true }] }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const n1 = ez.TestNode1();
|
|
||||||
const n2 = ez.TestNode2();
|
|
||||||
const p = ez.PrimitiveNode();
|
|
||||||
p.outputs[0].connectTo(n1.inputs[0]);
|
|
||||||
p.outputs[0].connectTo(n2.inputs[0]);
|
|
||||||
expect(p.outputs[0].connections).toHaveLength(2);
|
|
||||||
const valueWidget = p.widgets.value;
|
|
||||||
expect(valueWidget.widget.type).toBe("combo");
|
|
||||||
expect(valueWidget.widget.options.values).toEqual(["A", "B", "C"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("primitive can not connect to non matching combos on converted widgets", async () => {
|
|
||||||
const { ez } = await start({
|
|
||||||
mockNodeDefs: {
|
|
||||||
...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }),
|
|
||||||
...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const n1 = ez.TestNode1();
|
|
||||||
const n2 = ez.TestNode2();
|
|
||||||
const p = ez.PrimitiveNode();
|
|
||||||
p.outputs[0].connectTo(n1.inputs[0]);
|
|
||||||
expect(() => p.outputs[0].connectTo(n2.inputs[0])).toThrow();
|
|
||||||
expect(p.outputs[0].connections).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("combo output can not connect to non matching combos list input", async () => {
|
|
||||||
const { ez } = await start({
|
|
||||||
mockNodeDefs: {
|
|
||||||
...makeNodeDef("TestNode1", {}, [["A", "B"]]),
|
|
||||||
...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }),
|
|
||||||
...makeNodeDef("TestNode3", { example: [["A", "B", "C"], { forceInput: true }] }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const n1 = ez.TestNode1();
|
|
||||||
const n2 = ez.TestNode2();
|
|
||||||
const n3 = ez.TestNode3();
|
|
||||||
|
|
||||||
n1.outputs[0].connectTo(n2.inputs[0]);
|
|
||||||
expect(() => n1.outputs[0].connectTo(n3.inputs[0])).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("combo primitive can filter list when control_after_generate called", async () => {
|
|
||||||
const { ez } = await start({
|
|
||||||
mockNodeDefs: {
|
|
||||||
...makeNodeDef("TestNode1", { example: [["A", "B", "C", "D", "AA", "BB", "CC", "DD", "AAA", "BBB"], {}] }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const n1 = ez.TestNode1();
|
|
||||||
n1.widgets.example.convertToInput();
|
|
||||||
const p = ez.PrimitiveNode();
|
|
||||||
p.outputs[0].connectTo(n1.inputs[0]);
|
|
||||||
|
|
||||||
const value = p.widgets.value;
|
|
||||||
const control = p.widgets.control_after_generate.widget;
|
|
||||||
const filter = p.widgets.control_filter_list;
|
|
||||||
|
|
||||||
expect(p.widgets.length).toBe(3);
|
|
||||||
control.value = "increment";
|
|
||||||
expect(value.value).toBe("A");
|
|
||||||
|
|
||||||
// Manually trigger after queue when set to increment
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("B");
|
|
||||||
|
|
||||||
// Filter to items containing D
|
|
||||||
filter.value = "D";
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("D");
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("DD");
|
|
||||||
|
|
||||||
// Check decrement
|
|
||||||
value.value = "BBB";
|
|
||||||
control.value = "decrement";
|
|
||||||
filter.value = "B";
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("BB");
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("B");
|
|
||||||
|
|
||||||
// Check regex works
|
|
||||||
value.value = "BBB";
|
|
||||||
filter.value = "/[AB]|^C$/";
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("AAA");
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("BB");
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("AA");
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("C");
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("B");
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("A");
|
|
||||||
|
|
||||||
// Check random
|
|
||||||
control.value = "randomize";
|
|
||||||
filter.value = "/D/";
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value === "D" || value.value === "DD").toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure it doesnt apply when fixed
|
|
||||||
control.value = "fixed";
|
|
||||||
value.value = "B";
|
|
||||||
filter.value = "C";
|
|
||||||
control["afterQueued"]();
|
|
||||||
expect(value.value).toBe("B");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("reroutes", () => {
|
|
||||||
async function checkOutput(graph, values) {
|
|
||||||
expect((await graph.toPrompt()).output).toStrictEqual({
|
|
||||||
1: { inputs: { ckpt_name: "model1.safetensors" }, class_type: "CheckpointLoaderSimple" },
|
|
||||||
2: { inputs: { text: "positive", clip: ["1", 1] }, class_type: "CLIPTextEncode" },
|
|
||||||
3: { inputs: { text: "negative", clip: ["1", 1] }, class_type: "CLIPTextEncode" },
|
|
||||||
4: {
|
|
||||||
inputs: { width: values.width ?? 512, height: values.height ?? 512, batch_size: values?.batch_size ?? 1 },
|
|
||||||
class_type: "EmptyLatentImage",
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
inputs: {
|
|
||||||
seed: 0,
|
|
||||||
steps: 20,
|
|
||||||
cfg: 8,
|
|
||||||
sampler_name: "euler",
|
|
||||||
scheduler: values?.scheduler ?? "normal",
|
|
||||||
denoise: 1,
|
|
||||||
model: ["1", 0],
|
|
||||||
positive: ["2", 0],
|
|
||||||
negative: ["3", 0],
|
|
||||||
latent_image: ["4", 0],
|
|
||||||
},
|
|
||||||
class_type: "KSampler",
|
|
||||||
},
|
|
||||||
6: { inputs: { samples: ["5", 0], vae: ["1", 2] }, class_type: "VAEDecode" },
|
|
||||||
7: {
|
|
||||||
inputs: { filename_prefix: values.filename_prefix ?? "ComfyUI", images: ["6", 0] },
|
|
||||||
class_type: "SaveImage",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForWidget(node) {
|
|
||||||
// widgets are created slightly after the graph is ready
|
|
||||||
// hard to find an exact hook to get these so just wait for them to be ready
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
await new Promise((r) => setTimeout(r, 10));
|
|
||||||
if (node.widgets?.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
it("can connect primitive via a reroute path to a widget input", async () => {
|
|
||||||
const { ez, graph } = await start();
|
|
||||||
const nodes = createDefaultWorkflow(ez, graph);
|
|
||||||
|
|
||||||
nodes.empty.widgets.width.convertToInput();
|
|
||||||
nodes.sampler.widgets.scheduler.convertToInput();
|
|
||||||
nodes.save.widgets.filename_prefix.convertToInput();
|
|
||||||
|
|
||||||
let widthReroute = ez.Reroute();
|
|
||||||
let schedulerReroute = ez.Reroute();
|
|
||||||
let fileReroute = ez.Reroute();
|
|
||||||
|
|
||||||
let widthNext = widthReroute;
|
|
||||||
let schedulerNext = schedulerReroute;
|
|
||||||
let fileNext = fileReroute;
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
let next = ez.Reroute();
|
|
||||||
widthNext.outputs[0].connectTo(next.inputs[0]);
|
|
||||||
widthNext = next;
|
|
||||||
|
|
||||||
next = ez.Reroute();
|
|
||||||
schedulerNext.outputs[0].connectTo(next.inputs[0]);
|
|
||||||
schedulerNext = next;
|
|
||||||
|
|
||||||
next = ez.Reroute();
|
|
||||||
fileNext.outputs[0].connectTo(next.inputs[0]);
|
|
||||||
fileNext = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
widthNext.outputs[0].connectTo(nodes.empty.inputs.width);
|
|
||||||
schedulerNext.outputs[0].connectTo(nodes.sampler.inputs.scheduler);
|
|
||||||
fileNext.outputs[0].connectTo(nodes.save.inputs.filename_prefix);
|
|
||||||
|
|
||||||
let widthPrimitive = ez.PrimitiveNode();
|
|
||||||
let schedulerPrimitive = ez.PrimitiveNode();
|
|
||||||
let filePrimitive = ez.PrimitiveNode();
|
|
||||||
|
|
||||||
widthPrimitive.outputs[0].connectTo(widthReroute.inputs[0]);
|
|
||||||
schedulerPrimitive.outputs[0].connectTo(schedulerReroute.inputs[0]);
|
|
||||||
filePrimitive.outputs[0].connectTo(fileReroute.inputs[0]);
|
|
||||||
expect(widthPrimitive.widgets.value.value).toBe(512);
|
|
||||||
widthPrimitive.widgets.value.value = 1024;
|
|
||||||
expect(schedulerPrimitive.widgets.value.value).toBe("normal");
|
|
||||||
schedulerPrimitive.widgets.value.value = "simple";
|
|
||||||
expect(filePrimitive.widgets.value.value).toBe("ComfyUI");
|
|
||||||
filePrimitive.widgets.value.value = "ComfyTest";
|
|
||||||
|
|
||||||
await checkBeforeAndAfterReload(graph, async () => {
|
|
||||||
widthPrimitive = graph.find(widthPrimitive);
|
|
||||||
schedulerPrimitive = graph.find(schedulerPrimitive);
|
|
||||||
filePrimitive = graph.find(filePrimitive);
|
|
||||||
await waitForWidget(filePrimitive);
|
|
||||||
expect(widthPrimitive.widgets.length).toBe(2);
|
|
||||||
expect(schedulerPrimitive.widgets.length).toBe(3);
|
|
||||||
expect(filePrimitive.widgets.length).toBe(1);
|
|
||||||
|
|
||||||
await checkOutput(graph, {
|
|
||||||
width: 1024,
|
|
||||||
scheduler: "simple",
|
|
||||||
filename_prefix: "ComfyTest",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it("can connect primitive via a reroute path to multiple widget inputs", async () => {
|
|
||||||
const { ez, graph } = await start();
|
|
||||||
const nodes = createDefaultWorkflow(ez, graph);
|
|
||||||
|
|
||||||
nodes.empty.widgets.width.convertToInput();
|
|
||||||
nodes.empty.widgets.height.convertToInput();
|
|
||||||
nodes.empty.widgets.batch_size.convertToInput();
|
|
||||||
|
|
||||||
let reroute = ez.Reroute();
|
|
||||||
let prevReroute = reroute;
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const next = ez.Reroute();
|
|
||||||
prevReroute.outputs[0].connectTo(next.inputs[0]);
|
|
||||||
prevReroute = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
const r1 = ez.Reroute(prevReroute.outputs[0]);
|
|
||||||
const r2 = ez.Reroute(prevReroute.outputs[0]);
|
|
||||||
const r3 = ez.Reroute(r2.outputs[0]);
|
|
||||||
const r4 = ez.Reroute(r2.outputs[0]);
|
|
||||||
|
|
||||||
r1.outputs[0].connectTo(nodes.empty.inputs.width);
|
|
||||||
r3.outputs[0].connectTo(nodes.empty.inputs.height);
|
|
||||||
r4.outputs[0].connectTo(nodes.empty.inputs.batch_size);
|
|
||||||
|
|
||||||
let primitive = ez.PrimitiveNode();
|
|
||||||
primitive.outputs[0].connectTo(reroute.inputs[0]);
|
|
||||||
expect(primitive.widgets.value.value).toBe(1);
|
|
||||||
primitive.widgets.value.value = 64;
|
|
||||||
|
|
||||||
await checkBeforeAndAfterReload(graph, async (r) => {
|
|
||||||
primitive = graph.find(primitive);
|
|
||||||
await waitForWidget(primitive);
|
|
||||||
|
|
||||||
// Ensure widget configs are merged
|
|
||||||
expect(primitive.widgets.value.widget.options?.min).toBe(16); // width/height min
|
|
||||||
expect(primitive.widgets.value.widget.options?.max).toBe(4096); // batch max
|
|
||||||
expect(primitive.widgets.value.widget.options?.step).toBe(80); // width/height step * 10
|
|
||||||
|
|
||||||
await checkOutput(graph, {
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
batch_size: 64,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,452 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
/// <reference path="../../web/types/litegraph.d.ts" />
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef { import("../../web/scripts/app")["app"] } app
|
|
||||||
* @typedef { import("../../web/types/litegraph") } LG
|
|
||||||
* @typedef { import("../../web/types/litegraph").IWidget } IWidget
|
|
||||||
* @typedef { import("../../web/types/litegraph").ContextMenuItem } ContextMenuItem
|
|
||||||
* @typedef { import("../../web/types/litegraph").INodeInputSlot } INodeInputSlot
|
|
||||||
* @typedef { import("../../web/types/litegraph").INodeOutputSlot } INodeOutputSlot
|
|
||||||
* @typedef { InstanceType<LG["LGraphNode"]> & { widgets?: Array<IWidget> } } LGNode
|
|
||||||
* @typedef { (...args: EzOutput[] | [...EzOutput[], Record<string, unknown>]) => EzNode } EzNodeFactory
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class EzConnection {
|
|
||||||
/** @type { app } */
|
|
||||||
app;
|
|
||||||
/** @type { InstanceType<LG["LLink"]> } */
|
|
||||||
link;
|
|
||||||
|
|
||||||
get originNode() {
|
|
||||||
return new EzNode(this.app, this.app.graph.getNodeById(this.link.origin_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
get originOutput() {
|
|
||||||
return this.originNode.outputs[this.link.origin_slot];
|
|
||||||
}
|
|
||||||
|
|
||||||
get targetNode() {
|
|
||||||
return new EzNode(this.app, this.app.graph.getNodeById(this.link.target_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
get targetInput() {
|
|
||||||
return this.targetNode.inputs[this.link.target_slot];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { app } app
|
|
||||||
* @param { InstanceType<LG["LLink"]> } link
|
|
||||||
*/
|
|
||||||
constructor(app, link) {
|
|
||||||
this.app = app;
|
|
||||||
this.link = link;
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
this.targetInput.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EzSlot {
|
|
||||||
/** @type { EzNode } */
|
|
||||||
node;
|
|
||||||
/** @type { number } */
|
|
||||||
index;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { EzNode } node
|
|
||||||
* @param { number } index
|
|
||||||
*/
|
|
||||||
constructor(node, index) {
|
|
||||||
this.node = node;
|
|
||||||
this.index = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EzInput extends EzSlot {
|
|
||||||
/** @type { INodeInputSlot } */
|
|
||||||
input;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { EzNode } node
|
|
||||||
* @param { number } index
|
|
||||||
* @param { INodeInputSlot } input
|
|
||||||
*/
|
|
||||||
constructor(node, index, input) {
|
|
||||||
super(node, index);
|
|
||||||
this.input = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
get connection() {
|
|
||||||
const link = this.node.node.inputs?.[this.index]?.link;
|
|
||||||
if (link == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new EzConnection(this.node.app, this.node.app.graph.links[link]);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
this.node.node.disconnectInput(this.index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EzOutput extends EzSlot {
|
|
||||||
/** @type { INodeOutputSlot } */
|
|
||||||
output;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { EzNode } node
|
|
||||||
* @param { number } index
|
|
||||||
* @param { INodeOutputSlot } output
|
|
||||||
*/
|
|
||||||
constructor(node, index, output) {
|
|
||||||
super(node, index);
|
|
||||||
this.output = output;
|
|
||||||
}
|
|
||||||
|
|
||||||
get connections() {
|
|
||||||
return (this.node.node.outputs?.[this.index]?.links ?? []).map(
|
|
||||||
(l) => new EzConnection(this.node.app, this.node.app.graph.links[l])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { EzInput } input
|
|
||||||
*/
|
|
||||||
connectTo(input) {
|
|
||||||
if (!input) throw new Error("Invalid input");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type { LG["LLink"] | null }
|
|
||||||
*/
|
|
||||||
const link = this.node.node.connect(this.index, input.node.node, input.index);
|
|
||||||
if (!link) {
|
|
||||||
const inp = input.input;
|
|
||||||
const inName = inp.name || inp.label || inp.type;
|
|
||||||
throw new Error(
|
|
||||||
`Connecting from ${input.node.node.type}#${input.node.id}[${inName}#${input.index}] -> ${this.node.node.type}#${this.node.id}[${
|
|
||||||
this.output.name ?? this.output.type
|
|
||||||
}#${this.index}] failed.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return link;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EzNodeMenuItem {
|
|
||||||
/** @type { EzNode } */
|
|
||||||
node;
|
|
||||||
/** @type { number } */
|
|
||||||
index;
|
|
||||||
/** @type { ContextMenuItem } */
|
|
||||||
item;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { EzNode } node
|
|
||||||
* @param { number } index
|
|
||||||
* @param { ContextMenuItem } item
|
|
||||||
*/
|
|
||||||
constructor(node, index, item) {
|
|
||||||
this.node = node;
|
|
||||||
this.index = index;
|
|
||||||
this.item = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
call(selectNode = true) {
|
|
||||||
if (!this.item?.callback) throw new Error(`Menu Item ${this.item?.content ?? "[null]"} has no callback.`);
|
|
||||||
if (selectNode) {
|
|
||||||
this.node.select();
|
|
||||||
}
|
|
||||||
return this.item.callback.call(this.node.node, undefined, undefined, undefined, undefined, this.node.node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EzWidget {
|
|
||||||
/** @type { EzNode } */
|
|
||||||
node;
|
|
||||||
/** @type { number } */
|
|
||||||
index;
|
|
||||||
/** @type { IWidget } */
|
|
||||||
widget;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { EzNode } node
|
|
||||||
* @param { number } index
|
|
||||||
* @param { IWidget } widget
|
|
||||||
*/
|
|
||||||
constructor(node, index, widget) {
|
|
||||||
this.node = node;
|
|
||||||
this.index = index;
|
|
||||||
this.widget = widget;
|
|
||||||
}
|
|
||||||
|
|
||||||
get value() {
|
|
||||||
return this.widget.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
set value(v) {
|
|
||||||
this.widget.value = v;
|
|
||||||
this.widget.callback?.call?.(this.widget, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
get isConvertedToInput() {
|
|
||||||
// @ts-ignore : this type is valid for converted widgets
|
|
||||||
return this.widget.type === "converted-widget";
|
|
||||||
}
|
|
||||||
|
|
||||||
getConvertedInput() {
|
|
||||||
if (!this.isConvertedToInput) throw new Error(`Widget ${this.widget.name} is not converted to input.`);
|
|
||||||
|
|
||||||
return this.node.inputs.find((inp) => inp.input["widget"]?.name === this.widget.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
convertToWidget() {
|
|
||||||
if (!this.isConvertedToInput)
|
|
||||||
throw new Error(`Widget ${this.widget.name} cannot be converted as it is already a widget.`);
|
|
||||||
var menu = this.node.menu["Convert Input to Widget"].item.submenu.options;
|
|
||||||
var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to widget`);
|
|
||||||
menu[index].callback.call();
|
|
||||||
}
|
|
||||||
|
|
||||||
convertToInput() {
|
|
||||||
if (this.isConvertedToInput)
|
|
||||||
throw new Error(`Widget ${this.widget.name} cannot be converted as it is already an input.`);
|
|
||||||
var menu = this.node.menu["Convert Widget to Input"].item.submenu.options;
|
|
||||||
var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to input`);
|
|
||||||
menu[index].callback.call();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EzNode {
|
|
||||||
/** @type { app } */
|
|
||||||
app;
|
|
||||||
/** @type { LGNode } */
|
|
||||||
node;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { app } app
|
|
||||||
* @param { LGNode } node
|
|
||||||
*/
|
|
||||||
constructor(app, node) {
|
|
||||||
this.app = app;
|
|
||||||
this.node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
get id() {
|
|
||||||
return this.node.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
get inputs() {
|
|
||||||
return this.#makeLookupArray("inputs", "name", EzInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
get outputs() {
|
|
||||||
return this.#makeLookupArray("outputs", "name", EzOutput);
|
|
||||||
}
|
|
||||||
|
|
||||||
get widgets() {
|
|
||||||
return this.#makeLookupArray("widgets", "name", EzWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
get menu() {
|
|
||||||
return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
get isRemoved() {
|
|
||||||
return !this.app.graph.getNodeById(this.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
select(addToSelection = false) {
|
|
||||||
this.app.canvas.selectNode(this.node, addToSelection);
|
|
||||||
}
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * @template { "inputs" | "outputs" } T
|
|
||||||
// * @param { T } type
|
|
||||||
// * @returns { Record<string, type extends "inputs" ? EzInput : EzOutput> & (type extends "inputs" ? EzInput [] : EzOutput[]) }
|
|
||||||
// */
|
|
||||||
// #getSlotItems(type) {
|
|
||||||
// // @ts-ignore : these items are correct
|
|
||||||
// return (this.node[type] ?? []).reduce((p, s, i) => {
|
|
||||||
// if (s.name in p) {
|
|
||||||
// throw new Error(`Unable to store input ${s.name} on array as name conflicts.`);
|
|
||||||
// }
|
|
||||||
// // @ts-ignore
|
|
||||||
// p.push((p[s.name] = new (type === "inputs" ? EzInput : EzOutput)(this, i, s)));
|
|
||||||
// return p;
|
|
||||||
// }, Object.assign([], { $: this }));
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template { { new(node: EzNode, index: number, obj: any): any } } T
|
|
||||||
* @param { "inputs" | "outputs" | "widgets" | (() => Array<unknown>) } nodeProperty
|
|
||||||
* @param { string } nameProperty
|
|
||||||
* @param { T } ctor
|
|
||||||
* @returns { Record<string, InstanceType<T>> & Array<InstanceType<T>> }
|
|
||||||
*/
|
|
||||||
#makeLookupArray(nodeProperty, nameProperty, ctor) {
|
|
||||||
const items = typeof nodeProperty === "function" ? nodeProperty() : this.node[nodeProperty];
|
|
||||||
// @ts-ignore
|
|
||||||
return (items ?? []).reduce((p, s, i) => {
|
|
||||||
if (!s) return p;
|
|
||||||
|
|
||||||
const name = s[nameProperty];
|
|
||||||
const item = new ctor(this, i, s);
|
|
||||||
// @ts-ignore
|
|
||||||
p.push(item);
|
|
||||||
if (name) {
|
|
||||||
// @ts-ignore
|
|
||||||
if (name in p) {
|
|
||||||
throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
p[name] = item;
|
|
||||||
return p;
|
|
||||||
}, Object.assign([], { $: this }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EzGraph {
|
|
||||||
/** @type { app } */
|
|
||||||
app;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { app } app
|
|
||||||
*/
|
|
||||||
constructor(app) {
|
|
||||||
this.app = app;
|
|
||||||
}
|
|
||||||
|
|
||||||
get nodes() {
|
|
||||||
return this.app.graph._nodes.map((n) => new EzNode(this.app, n));
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.app.graph.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
arrange() {
|
|
||||||
this.app.graph.arrange();
|
|
||||||
}
|
|
||||||
|
|
||||||
stringify() {
|
|
||||||
return JSON.stringify(this.app.graph.serialize(), undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { number | LGNode | EzNode } obj
|
|
||||||
* @returns { EzNode }
|
|
||||||
*/
|
|
||||||
find(obj) {
|
|
||||||
let match;
|
|
||||||
let id;
|
|
||||||
if (typeof obj === "number") {
|
|
||||||
id = obj;
|
|
||||||
} else {
|
|
||||||
id = obj.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
match = this.app.graph.getNodeById(id);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
throw new Error(`Unable to find node with ID ${id}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EzNode(this.app, match);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns { Promise<void> }
|
|
||||||
*/
|
|
||||||
reload() {
|
|
||||||
const graph = JSON.parse(JSON.stringify(this.app.graph.serialize()));
|
|
||||||
return new Promise((r) => {
|
|
||||||
this.app.graph.clear();
|
|
||||||
setTimeout(async () => {
|
|
||||||
await this.app.loadGraphData(graph);
|
|
||||||
r();
|
|
||||||
}, 10);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns { Promise<{
|
|
||||||
* workflow: {},
|
|
||||||
* output: Record<string, {
|
|
||||||
* class_name: string,
|
|
||||||
* inputs: Record<string, [string, number] | unknown>
|
|
||||||
* }>}> }
|
|
||||||
*/
|
|
||||||
toPrompt() {
|
|
||||||
// @ts-ignore
|
|
||||||
return this.app.graphToPrompt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Ez = {
|
|
||||||
/**
|
|
||||||
* Quickly build and interact with a ComfyUI graph
|
|
||||||
* @example
|
|
||||||
* const { ez, graph } = Ez.graph(app);
|
|
||||||
* graph.clear();
|
|
||||||
* const [model, clip, vae] = ez.CheckpointLoaderSimple().outputs;
|
|
||||||
* const [pos] = ez.CLIPTextEncode(clip, { text: "positive" }).outputs;
|
|
||||||
* const [neg] = ez.CLIPTextEncode(clip, { text: "negative" }).outputs;
|
|
||||||
* const [latent] = ez.KSampler(model, pos, neg, ...ez.EmptyLatentImage().outputs).outputs;
|
|
||||||
* const [image] = ez.VAEDecode(latent, vae).outputs;
|
|
||||||
* const saveNode = ez.SaveImage(image);
|
|
||||||
* console.log(saveNode);
|
|
||||||
* graph.arrange();
|
|
||||||
* @param { app } app
|
|
||||||
* @param { LG["LiteGraph"] } LiteGraph
|
|
||||||
* @param { LG["LGraphCanvas"] } LGraphCanvas
|
|
||||||
* @param { boolean } clearGraph
|
|
||||||
* @returns { { graph: EzGraph, ez: Record<string, EzNodeFactory> } }
|
|
||||||
*/
|
|
||||||
graph(app, LiteGraph = window["LiteGraph"], LGraphCanvas = window["LGraphCanvas"], clearGraph = true) {
|
|
||||||
// Always set the active canvas so things work
|
|
||||||
LGraphCanvas.active_canvas = app.canvas;
|
|
||||||
|
|
||||||
if (clearGraph) {
|
|
||||||
app.graph.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore : this proxy handles utility methods & node creation
|
|
||||||
const factory = new Proxy(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
get(_, p) {
|
|
||||||
if (typeof p !== "string") throw new Error("Invalid node");
|
|
||||||
const node = LiteGraph.createNode(p);
|
|
||||||
if (!node) throw new Error(`Unknown node "${p}"`);
|
|
||||||
app.graph.add(node);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Parameters<EzNodeFactory>} args
|
|
||||||
*/
|
|
||||||
return function (...args) {
|
|
||||||
const ezNode = new EzNode(app, node);
|
|
||||||
const inputs = ezNode.inputs;
|
|
||||||
|
|
||||||
let slot = 0;
|
|
||||||
for (const arg of args) {
|
|
||||||
if (arg instanceof EzOutput) {
|
|
||||||
arg.connectTo(inputs[slot++]);
|
|
||||||
} else {
|
|
||||||
for (const k in arg) {
|
|
||||||
ezNode.widgets[k].value = arg[k];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ezNode;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return { graph: new EzGraph(app), ez: factory };
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,129 +0,0 @@
|
||||||
const { mockApi } = require("./setup");
|
|
||||||
const { Ez } = require("./ezgraph");
|
|
||||||
const lg = require("./litegraph");
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const html = fs.readFileSync(path.resolve(__dirname, "../../web/index.html"))
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param { Parameters<typeof mockApi>[0] & {
|
|
||||||
* resetEnv?: boolean,
|
|
||||||
* preSetup?(app): Promise<void>,
|
|
||||||
* localStorage?: Record<string, string>
|
|
||||||
* } } config
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export async function start(config = {}) {
|
|
||||||
if(config.resetEnv) {
|
|
||||||
jest.resetModules();
|
|
||||||
jest.resetAllMocks();
|
|
||||||
lg.setup(global);
|
|
||||||
localStorage.clear();
|
|
||||||
sessionStorage.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(localStorage, config.localStorage ?? {});
|
|
||||||
document.body.innerHTML = html;
|
|
||||||
|
|
||||||
mockApi(config);
|
|
||||||
const { app } = require("../../web/scripts/app");
|
|
||||||
config.preSetup?.(app);
|
|
||||||
await app.setup();
|
|
||||||
|
|
||||||
return { ...Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]), app };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { ReturnType<Ez["graph"]>["graph"] } graph
|
|
||||||
* @param { (hasReloaded: boolean) => (Promise<void> | void) } cb
|
|
||||||
*/
|
|
||||||
export async function checkBeforeAndAfterReload(graph, cb) {
|
|
||||||
await cb(false);
|
|
||||||
await graph.reload();
|
|
||||||
await cb(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { string } name
|
|
||||||
* @param { Record<string, string | [string | string[], any]> } input
|
|
||||||
* @param { (string | string[])[] | Record<string, string | string[]> } output
|
|
||||||
* @returns { Record<string, import("../../web/types/comfy").ComfyObjectInfo> }
|
|
||||||
*/
|
|
||||||
export function makeNodeDef(name, input, output = {}) {
|
|
||||||
const nodeDef = {
|
|
||||||
name,
|
|
||||||
category: "test",
|
|
||||||
output: [],
|
|
||||||
output_name: [],
|
|
||||||
output_is_list: [],
|
|
||||||
input: {
|
|
||||||
required: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
for (const k in input) {
|
|
||||||
nodeDef.input.required[k] = typeof input[k] === "string" ? [input[k], {}] : [...input[k]];
|
|
||||||
}
|
|
||||||
if (output instanceof Array) {
|
|
||||||
output = output.reduce((p, c) => {
|
|
||||||
p[c] = c;
|
|
||||||
return p;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
for (const k in output) {
|
|
||||||
nodeDef.output.push(output[k]);
|
|
||||||
nodeDef.output_name.push(k);
|
|
||||||
nodeDef.output_is_list.push(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { [name]: nodeDef };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
/**
|
|
||||||
* @template { any } T
|
|
||||||
* @param { T } x
|
|
||||||
* @returns { x is Exclude<T, null | undefined> }
|
|
||||||
*/
|
|
||||||
export function assertNotNullOrUndefined(x) {
|
|
||||||
expect(x).not.toEqual(null);
|
|
||||||
expect(x).not.toEqual(undefined);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param { ReturnType<Ez["graph"]>["ez"] } ez
|
|
||||||
* @param { ReturnType<Ez["graph"]>["graph"] } graph
|
|
||||||
*/
|
|
||||||
export function createDefaultWorkflow(ez, graph) {
|
|
||||||
graph.clear();
|
|
||||||
const ckpt = ez.CheckpointLoaderSimple();
|
|
||||||
|
|
||||||
const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" });
|
|
||||||
const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" });
|
|
||||||
|
|
||||||
const empty = ez.EmptyLatentImage();
|
|
||||||
const sampler = ez.KSampler(
|
|
||||||
ckpt.outputs.MODEL,
|
|
||||||
pos.outputs.CONDITIONING,
|
|
||||||
neg.outputs.CONDITIONING,
|
|
||||||
empty.outputs.LATENT
|
|
||||||
);
|
|
||||||
|
|
||||||
const decode = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE);
|
|
||||||
const save = ez.SaveImage(decode.outputs.IMAGE);
|
|
||||||
graph.arrange();
|
|
||||||
|
|
||||||
return { ckpt, pos, neg, empty, sampler, decode, save };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getNodeDefs() {
|
|
||||||
const { api } = require("../../web/scripts/api");
|
|
||||||
return api.getNodeDefs();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getNodeDef(nodeId) {
|
|
||||||
return (await getNodeDefs())[nodeId];
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const { nop } = require("../utils/nopProxy");
|
|
||||||
|
|
||||||
function forEachKey(cb) {
|
|
||||||
for (const k of [
|
|
||||||
"LiteGraph",
|
|
||||||
"LGraph",
|
|
||||||
"LLink",
|
|
||||||
"LGraphNode",
|
|
||||||
"LGraphGroup",
|
|
||||||
"DragAndScale",
|
|
||||||
"LGraphCanvas",
|
|
||||||
"ContextMenu",
|
|
||||||
]) {
|
|
||||||
cb(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setup(ctx) {
|
|
||||||
const lg = fs.readFileSync(path.resolve("../web/lib/litegraph.core.js"), "utf-8");
|
|
||||||
const globalTemp = {};
|
|
||||||
(function (console) {
|
|
||||||
eval(lg);
|
|
||||||
}).call(globalTemp, nop);
|
|
||||||
|
|
||||||
forEachKey((k) => (ctx[k] = globalTemp[k]));
|
|
||||||
require(path.resolve("../web/lib/litegraph.extensions.js"));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function teardown(ctx) {
|
|
||||||
forEachKey((k) => delete ctx[k]);
|
|
||||||
|
|
||||||
// Clear document after each run
|
|
||||||
document.getElementsByTagName("html")[0].innerHTML = "";
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
export const nop = new Proxy(function () {}, {
|
|
||||||
get: () => nop,
|
|
||||||
set: () => true,
|
|
||||||
apply: () => nop,
|
|
||||||
construct: () => nop,
|
|
||||||
});
|
|
|
@ -1,82 +0,0 @@
|
||||||
require("../../web/scripts/api");
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
function* walkSync(dir) {
|
|
||||||
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
yield* walkSync(path.join(dir, file.name));
|
|
||||||
} else {
|
|
||||||
yield path.join(dir, file.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef { import("../../web/types/comfy").ComfyObjectInfo } ComfyObjectInfo
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {{
|
|
||||||
* mockExtensions?: string[],
|
|
||||||
* mockNodeDefs?: Record<string, ComfyObjectInfo>,
|
|
||||||
* settings?: Record<string, string>
|
|
||||||
* userConfig?: {storage: "server" | "browser", users?: Record<string, any>, migrated?: boolean },
|
|
||||||
* userData?: Record<string, any>
|
|
||||||
* }} config
|
|
||||||
*/
|
|
||||||
export function mockApi(config = {}) {
|
|
||||||
let { mockExtensions, mockNodeDefs, userConfig, settings, userData } = {
|
|
||||||
userConfig,
|
|
||||||
settings: {},
|
|
||||||
userData: {},
|
|
||||||
...config,
|
|
||||||
};
|
|
||||||
if (!mockExtensions) {
|
|
||||||
mockExtensions = Array.from(walkSync(path.resolve("../web/extensions/core")))
|
|
||||||
.filter((x) => x.endsWith(".js"))
|
|
||||||
.map((x) => path.relative(path.resolve("../web"), x));
|
|
||||||
}
|
|
||||||
if (!mockNodeDefs) {
|
|
||||||
mockNodeDefs = JSON.parse(fs.readFileSync(path.resolve("./data/object_info.json")));
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = new EventTarget();
|
|
||||||
const mockApi = {
|
|
||||||
addEventListener: events.addEventListener.bind(events),
|
|
||||||
removeEventListener: events.removeEventListener.bind(events),
|
|
||||||
dispatchEvent: events.dispatchEvent.bind(events),
|
|
||||||
getSystemStats: jest.fn(),
|
|
||||||
getExtensions: jest.fn(() => mockExtensions),
|
|
||||||
getNodeDefs: jest.fn(() => mockNodeDefs),
|
|
||||||
init: jest.fn(),
|
|
||||||
apiURL: jest.fn((x) => "../../web/" + x),
|
|
||||||
createUser: jest.fn((username) => {
|
|
||||||
if(username in userConfig.users) {
|
|
||||||
return { status: 400, json: () => "Duplicate" }
|
|
||||||
}
|
|
||||||
userConfig.users[username + "!"] = username;
|
|
||||||
return { status: 200, json: () => username + "!" }
|
|
||||||
}),
|
|
||||||
getUserConfig: jest.fn(() => userConfig ?? { storage: "browser", migrated: false }),
|
|
||||||
getSettings: jest.fn(() => settings),
|
|
||||||
storeSettings: jest.fn((v) => Object.assign(settings, v)),
|
|
||||||
getUserData: jest.fn((f) => {
|
|
||||||
if (f in userData) {
|
|
||||||
return { status: 200, json: () => userData[f] };
|
|
||||||
} else {
|
|
||||||
return { status: 404 };
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
storeUserData: jest.fn((file, data) => {
|
|
||||||
userData[file] = data;
|
|
||||||
}),
|
|
||||||
listUserData: jest.fn(() => [])
|
|
||||||
};
|
|
||||||
jest.mock("../../web/scripts/api", () => ({
|
|
||||||
get api() {
|
|
||||||
return mockApi;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
Loading…
Reference in New Issue