Format everything (#211)

This commit is contained in:
Chenlei Hu
2024-07-23 15:40:54 -04:00
committed by GitHub
parent 648e52e39c
commit 1b7db43f8a
25 changed files with 1014 additions and 526 deletions

View File

@@ -54,7 +54,7 @@ export class ComfyPage {
this.canvas = page.locator("#graph-canvas");
this.widgetTextBox = page.getByPlaceholder("text").nth(1);
this.resetViewButton = page.getByRole("button", { name: "Reset View" });
this.workflowUploadInput = page.locator('#comfy-file-input');
this.workflowUploadInput = page.locator("#comfy-file-input");
this.searchBox = new ComfyNodeSearchBox(page);
}
@@ -69,7 +69,9 @@ export class ComfyPage {
}
async loadWorkflow(workflowName: string) {
await this.workflowUploadInput.setInputFiles(`./browser_tests/assets/${workflowName}.json`);
await this.workflowUploadInput.setInputFiles(
`./browser_tests/assets/${workflowName}.json`
);
await this.nextFrame();
}
@@ -234,15 +236,21 @@ export class ComfyPage {
await this.nextFrame();
}
async resizeNode(nodePos: Position, nodeSize: Size, ratioX: number, ratioY: number, revertAfter: boolean = false) {
async resizeNode(
nodePos: Position,
nodeSize: Size,
ratioX: number,
ratioY: number,
revertAfter: boolean = false
) {
const bottomRight = {
x: nodePos.x + nodeSize.width,
y: nodePos.y + nodeSize.height,
}
};
const target = {
x: nodePos.x + nodeSize.width * ratioX,
y: nodePos.y + nodeSize.height * ratioY,
}
};
await this.dragAndDrop(bottomRight, target);
await this.nextFrame();
if (revertAfter) {
@@ -251,42 +259,65 @@ export class ComfyPage {
}
}
async resizeKsamplerNode(percentX: number, percentY: number, revertAfter: boolean = false) {
async resizeKsamplerNode(
percentX: number,
percentY: number,
revertAfter: boolean = false
) {
const ksamplerPos = {
x: 864,
y: 157
}
y: 157,
};
const ksamplerSize = {
width: 315,
height: 292,
}
};
this.resizeNode(ksamplerPos, ksamplerSize, percentX, percentY, revertAfter);
}
async resizeLoadCheckpointNode(percentX: number, percentY: number, revertAfter: boolean = false) {
async resizeLoadCheckpointNode(
percentX: number,
percentY: number,
revertAfter: boolean = false
) {
const loadCheckpointPos = {
x: 25,
y: 440,
}
};
const loadCheckpointSize = {
width: 320,
height: 120,
}
this.resizeNode(loadCheckpointPos, loadCheckpointSize, percentX, percentY, revertAfter);
};
this.resizeNode(
loadCheckpointPos,
loadCheckpointSize,
percentX,
percentY,
revertAfter
);
}
async resizeEmptyLatentNode(percentX: number, percentY: number, revertAfter: boolean = false) {
async resizeEmptyLatentNode(
percentX: number,
percentY: number,
revertAfter: boolean = false
) {
const emptyLatentPos = {
x: 475,
y: 580,
}
};
const emptyLatentSize = {
width: 303,
height: 132,
}
this.resizeNode(emptyLatentPos, emptyLatentSize, percentX, percentY, revertAfter);
};
this.resizeNode(
emptyLatentPos,
emptyLatentSize,
percentX,
percentY,
revertAfter
);
}
}
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({

View File

@@ -1,71 +1,77 @@
import { expect } from '@playwright/test';
import { ComfyPage, comfyPageFixture as test } from './ComfyPage';
import { expect } from "@playwright/test";
import { ComfyPage, comfyPageFixture as test } from "./ComfyPage";
test.describe('Node Interaction', () => {
test('Can enter prompt', async ({ comfyPage }) => {
test.describe("Node Interaction", () => {
test("Can enter prompt", async ({ comfyPage }) => {
const textBox = comfyPage.widgetTextBox;
await textBox.click();
await textBox.fill('Hello World');
await expect(textBox).toHaveValue('Hello World');
await textBox.fill('Hello World 2');
await expect(textBox).toHaveValue('Hello World 2');
await textBox.fill("Hello World");
await expect(textBox).toHaveValue("Hello World");
await textBox.fill("Hello World 2");
await expect(textBox).toHaveValue("Hello World 2");
});
test('Can highlight selected', async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png');
test("Can highlight selected", async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot("default.png");
await comfyPage.clickTextEncodeNode1();
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png');
await expect(comfyPage.canvas).toHaveScreenshot("selected-node1.png");
await comfyPage.clickTextEncodeNode2();
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png');
await expect(comfyPage.canvas).toHaveScreenshot("selected-node2.png");
});
// Flaky. See https://github.com/comfyanonymous/ComfyUI/issues/3866
test.skip('Can drag node', async ({ comfyPage }) => {
test.skip("Can drag node", async ({ comfyPage }) => {
await comfyPage.dragNode2();
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png');
await expect(comfyPage.canvas).toHaveScreenshot("dragged-node1.png");
});
test('Can disconnect/connect edge', async ({ comfyPage }) => {
test("Can disconnect/connect edge", async ({ comfyPage }) => {
await comfyPage.disconnectEdge();
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge-with-menu.png');
await expect(comfyPage.canvas).toHaveScreenshot(
"disconnected-edge-with-menu.png"
);
await comfyPage.connectEdge();
// Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', { maxDiffPixels: 50 });
await expect(comfyPage.canvas).toHaveScreenshot("default.png", {
maxDiffPixels: 50,
});
});
test('Can adjust widget value', async ({ comfyPage }) => {
test("Can adjust widget value", async ({ comfyPage }) => {
await comfyPage.adjustWidgetValue();
await expect(comfyPage.canvas).toHaveScreenshot('adjusted-widget-value.png');
await expect(comfyPage.canvas).toHaveScreenshot(
"adjusted-widget-value.png"
);
});
test('Link snap to slot', async ({comfyPage}) => {
test("Link snap to slot", async ({ comfyPage }) => {
await comfyPage.loadWorkflow("snap_to_slot");
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png');
await expect(comfyPage.canvas).toHaveScreenshot("snap_to_slot.png");
const outputSlotPos = {
x: 406,
y: 333
y: 333,
};
const samplerNodeCenterPos = {
x: 748,
y: 77
y: 77,
};
await comfyPage.dragAndDrop(outputSlotPos, samplerNodeCenterPos);
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png');
await expect(comfyPage.canvas).toHaveScreenshot("snap_to_slot_linked.png");
});
});
test.describe('Canvas Interaction', () => {
test('Can zoom in/out', async ({ comfyPage }) => {
test.describe("Canvas Interaction", () => {
test("Can zoom in/out", async ({ comfyPage }) => {
await comfyPage.zoom(-100);
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png');
await expect(comfyPage.canvas).toHaveScreenshot("zoomed-in.png");
await comfyPage.zoom(200);
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out.png');
await expect(comfyPage.canvas).toHaveScreenshot("zoomed-out.png");
});
test('Can pan', async ({ comfyPage }) => {
test("Can pan", async ({ comfyPage }) => {
await comfyPage.pan({ x: 200, y: 200 });
await expect(comfyPage.canvas).toHaveScreenshot('panned.png');
await expect(comfyPage.canvas).toHaveScreenshot("panned.png");
});
});

View File

@@ -3,7 +3,9 @@ import { comfyPageFixture as test } from "./ComfyPage";
function listenForEvent(): Promise<Event> {
return new Promise<Event>((resolve) => {
document.addEventListener("litegraph:canvas", (e) => resolve(e), { once: true });
document.addEventListener("litegraph:canvas", (e) => resolve(e), {
once: true,
});
});
}

View File

@@ -1,6 +1,5 @@
import { expect } from "@playwright/test";
import { comfyPageFixture as test} from "./ComfyPage";
import { comfyPageFixture as test } from "./ComfyPage";
test.describe("Node search box", () => {
test("Can trigger on empty canvas double click", async ({ comfyPage }) => {

View File

@@ -1,78 +1,92 @@
import { expect } from '@playwright/test';
import { comfyPageFixture as test } from './ComfyPage';
import { expect } from "@playwright/test";
import { comfyPageFixture as test } from "./ComfyPage";
test.describe('Canvas Right Click Menu', () => {
// See https://github.com/comfyanonymous/ComfyUI/issues/3883
// Right-click menu on canvas's option sequence is not stable.
test.skip('Can add node', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas();
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png');
await comfyPage.page.getByText('Add Node').click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot('add-node-menu.png');
await comfyPage.page.getByText('loaders').click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot('add-node-menu-loaders.png');
await comfyPage.page.getByText('Load VAE').click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png');
});
test.describe("Canvas Right Click Menu", () => {
// See https://github.com/comfyanonymous/ComfyUI/issues/3883
// Right-click menu on canvas's option sequence is not stable.
test.skip("Can add node", async ({ comfyPage }) => {
await comfyPage.rightClickCanvas();
await expect(comfyPage.canvas).toHaveScreenshot("right-click-menu.png");
await comfyPage.page.getByText("Add Node").click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot("add-node-menu.png");
await comfyPage.page.getByText("loaders").click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot(
"add-node-menu-loaders.png"
);
await comfyPage.page.getByText("Load VAE").click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot("add-node-node-added.png");
});
// See https://github.com/comfyanonymous/ComfyUI/issues/3883
// Right-click menu on canvas's option sequence is not stable.
test.skip('Can add group', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas();
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png');
await comfyPage.page.getByText('Add Group', { exact: true }).click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png');
});
// See https://github.com/comfyanonymous/ComfyUI/issues/3883
// Right-click menu on canvas's option sequence is not stable.
test.skip("Can add group", async ({ comfyPage }) => {
await comfyPage.rightClickCanvas();
await expect(comfyPage.canvas).toHaveScreenshot("right-click-menu.png");
await comfyPage.page.getByText("Add Group", { exact: true }).click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot(
"add-group-group-added.png"
);
});
test('Can convert to group node', async ({ comfyPage }) => {
await comfyPage.select2Nodes();
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png');
comfyPage.page.on('dialog', async dialog => {
await dialog.accept("GroupNode2CLIP");
});
await comfyPage.rightClickCanvas();
await comfyPage.page.getByText('Convert to Group Node').click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node-group-node.png');
test("Can convert to group node", async ({ comfyPage }) => {
await comfyPage.select2Nodes();
await expect(comfyPage.canvas).toHaveScreenshot("selected-2-nodes.png");
comfyPage.page.on("dialog", async (dialog) => {
await dialog.accept("GroupNode2CLIP");
});
await comfyPage.rightClickCanvas();
await comfyPage.page.getByText("Convert to Group Node").click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot(
"right-click-node-group-node.png"
);
});
});
test.describe('Node Right Click Menu', () => {
test('Can open properties panel', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode();
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png');
await comfyPage.page.getByText('Properties Panel').click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node-properties-panel.png');
});
test.describe("Node Right Click Menu", () => {
test("Can open properties panel", async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode();
await expect(comfyPage.canvas).toHaveScreenshot("right-click-node.png");
await comfyPage.page.getByText("Properties Panel").click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot(
"right-click-node-properties-panel.png"
);
});
test('Can collapse', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode();
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png');
await comfyPage.page.getByText('Collapse').click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node-collapsed.png');
});
test("Can collapse", async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode();
await expect(comfyPage.canvas).toHaveScreenshot("right-click-node.png");
await comfyPage.page.getByText("Collapse").click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot(
"right-click-node-collapsed.png"
);
});
test('Can bypass', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode();
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png');
await comfyPage.page.getByText('Bypass').click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node-bypassed.png');
});
test("Can bypass", async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode();
await expect(comfyPage.canvas).toHaveScreenshot("right-click-node.png");
await comfyPage.page.getByText("Bypass").click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot(
"right-click-node-bypassed.png"
);
});
test('Can convert widget to input', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode();
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png');
await comfyPage.page.getByText('Convert Widget to Input').click();
await comfyPage.nextFrame();
await comfyPage.page.getByText('Convert width to input').click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node-widget-converted.png');
});
test("Can convert widget to input", async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode();
await expect(comfyPage.canvas).toHaveScreenshot("right-click-node.png");
await comfyPage.page.getByText("Convert Widget to Input").click();
await comfyPage.nextFrame();
await comfyPage.page.getByText("Convert width to input").click();
await comfyPage.nextFrame();
await expect(comfyPage.canvas).toHaveScreenshot(
"right-click-node-widget-converted.png"
);
});
});

View File

@@ -1,23 +1,26 @@
import type { JestConfigWithTsJest } from "ts-jest";
const jestConfig: JestConfigWithTsJest = {
testMatch: ["**/tests-ui/**/*.test.ts"],
testEnvironment: "jsdom",
transform: {
'^.+\\.m?[tj]sx?$': ["ts-jest", {
tsconfig: "./tsconfig.json",
babelConfig: "./babel.config.json",
}],
},
setupFiles: ["./tests-ui/globalSetup.ts"],
setupFilesAfterEnv: ["./tests-ui/afterSetup.ts"],
clearMocks: true,
resetModules: true,
testTimeout: 10000,
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
testMatch: ["**/tests-ui/**/*.test.ts"],
testEnvironment: "jsdom",
transform: {
"^.+\\.m?[tj]sx?$": [
"ts-jest",
{
tsconfig: "./tsconfig.json",
babelConfig: "./babel.config.json",
},
],
},
setupFiles: ["./tests-ui/globalSetup.ts"],
setupFilesAfterEnv: ["./tests-ui/afterSetup.ts"],
clearMocks: true,
resetModules: true,
testTimeout: 10000,
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
},
};
export default jestConfig;
export default jestConfig;

View File

@@ -9,7 +9,7 @@
"deploy": "npm run build && node scripts/deploy.js",
"zipdist": "node scripts/zipdist.js",
"typecheck": "tsc --noEmit",
"format": "prettier --write 'src/**/*.{js,ts,tsx,vue}'",
"format": "prettier --write './**/*.{js,ts,tsx,vue}'",
"test": "npm run build && jest",
"test:generate:examples": "npx tsx tests-ui/extractExamples",
"test:generate": "npx tsx tests-ui/setup",
@@ -63,7 +63,7 @@
"zod-validation-error": "^3.3.0"
},
"lint-staged": {
"src/**/*.{js,ts,tsx,vue}": [
"./**/*.{js,ts,tsx,vue}": [
"prettier --write",
"git add"
]

View File

@@ -1,4 +1,4 @@
import { defineConfig, devices } from '@playwright/test';
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
@@ -11,7 +11,7 @@ import { defineConfig, devices } from '@playwright/test';
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './browser_tests',
testDir: "./browser_tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
@@ -21,21 +21,21 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
name: "chromium",
use: { ...devices["Desktop Chrome"] },
timeout: 5000,
},

View File

@@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@@ -1,8 +1,8 @@
import { copy } from 'fs-extra';
import { copy } from "fs-extra";
import { config } from "dotenv";
config();
const sourceDir = './dist';
const sourceDir = "./dist";
const targetDir = process.env.DEPLOY_COMFYUI_DIR;
copy(sourceDir, targetDir)
@@ -10,5 +10,5 @@ copy(sourceDir, targetDir)
console.log(`Directory copied successfully! ${sourceDir} -> ${targetDir}`);
})
.catch((err) => {
console.error('Error copying directory:', err);
});
console.error("Error copying directory:", err);
});

View File

@@ -1,9 +1,9 @@
import zipdir from 'zip-dir';
import zipdir from "zip-dir";
zipdir('./dist', { saveTo: './dist.zip' }, function (err, buffer) {
zipdir("./dist", { saveTo: "./dist.zip" }, function (err, buffer) {
if (err) {
console.error('Error zipping "dist" directory:', err);
} else {
console.log('Successfully zipped "dist" directory.');
}
});
});

View File

@@ -13,7 +13,7 @@ export class ChangeTracker {
isOurLoad = false;
workflow: ComfyWorkflow | null;
ds: { scale: number; offset: [number, number]; };
ds: { scale: number; offset: [number, number] };
nodeOutputs: any;
get app() {

View File

@@ -24,7 +24,10 @@ export class ComfyWorkflowManager extends EventTarget {
workflowLookup: Record<string, ComfyWorkflow> = {};
workflows: Array<ComfyWorkflow> = [];
openWorkflows: Array<ComfyWorkflow> = [];
queuedPrompts: Record<string, { workflow?: ComfyWorkflow; nodes?: Record<string, boolean>; }> = {};
queuedPrompts: Record<
string,
{ workflow?: ComfyWorkflow; nodes?: Record<string, boolean> }
> = {};
app: ComfyApp;
get activeWorkflow() {
@@ -254,7 +257,12 @@ export class ComfyWorkflow {
return !!this.changeTracker;
}
constructor(manager: ComfyWorkflowManager, path: string, pathParts?: string[], isFavorite?: boolean) {
constructor(
manager: ComfyWorkflowManager,
path: string,
pathParts?: string[],
isFavorite?: boolean
) {
this.manager = manager;
if (pathParts) {
this.#updatePath(path, pathParts);

View File

@@ -1,12 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
}
};

View File

@@ -13,8 +13,14 @@ async function setup() {
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"];
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");
@@ -24,7 +30,9 @@ async function setup() {
}
const outPath = resolve(outDir, "object_info.json");
console.log(`Writing ${Object.keys(objectInfo).length} nodes to ${outPath}`);
console.log(
`Writing ${Object.keys(objectInfo).length} nodes to ${outPath}`
);
writeFileSync(outPath, data, {
encoding: "utf8",
});
@@ -35,4 +43,4 @@ async function setup() {
});
}
setup();
setup();

View File

@@ -5,87 +5,117 @@ import fs from "fs";
const WORKFLOW_DIR = "tests-ui/workflows";
describe("parseComfyWorkflow", () => {
it("parses valid workflow", async () => {
fs.readdirSync(WORKFLOW_DIR).forEach(async (file) => {
if (file.endsWith(".json")) {
const data = fs.readFileSync(`${WORKFLOW_DIR}/${file}`, "utf-8");
await expect(parseComfyWorkflow(data)).resolves.not.toThrow();
}
});
it("parses valid workflow", async () => {
fs.readdirSync(WORKFLOW_DIR).forEach(async (file) => {
if (file.endsWith(".json")) {
const data = fs.readFileSync(`${WORKFLOW_DIR}/${file}`, "utf-8");
await expect(parseComfyWorkflow(data)).resolves.not.toThrow();
}
});
});
it("workflow.nodes", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.nodes = undefined;
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow();
it("workflow.nodes", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.nodes = undefined;
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
workflow.nodes = null;
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow();
workflow.nodes = null;
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
workflow.nodes = [];
await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow();
});
workflow.nodes = [];
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
});
it("workflow.version", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.version = undefined;
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow();
it("workflow.version", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.version = undefined;
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
workflow.version = "1.0.1"; // Invalid format.
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow();
workflow.version = "1.0.1"; // Invalid format.
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
workflow.version = 1;
await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow();
});
workflow.version = 1;
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
});
it("workflow.extra", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.extra = undefined;
await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow();
it("workflow.extra", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.extra = undefined;
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
workflow.extra = null;
await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow();
workflow.extra = null;
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
workflow.extra = {};
await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow();
workflow.extra = {};
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
workflow.extra = { foo: "bar" }; // Should accept extra fields.
await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow();
});
workflow.extra = { foo: "bar" }; // Should accept extra fields.
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
});
it("workflow.nodes.pos", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.nodes[0].pos = [1, 2, 3];
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow();
it("workflow.nodes.pos", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.nodes[0].pos = [1, 2, 3];
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
workflow.nodes[0].pos = [1, 2];
await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow();
workflow.nodes[0].pos = [1, 2];
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
// Should automatically transform the legacy format object to array.
workflow.nodes[0].pos = {"0": 3, "1": 4};
let parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow));
expect(parsedWorkflow.nodes[0].pos).toEqual([3, 4]);
// Should automatically transform the legacy format object to array.
workflow.nodes[0].pos = { "0": 3, "1": 4 };
let parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow));
expect(parsedWorkflow.nodes[0].pos).toEqual([3, 4]);
workflow.nodes[0].pos = {0: 3, 1: 4};
parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow));
expect(parsedWorkflow.nodes[0].pos).toEqual([3, 4]);
});
workflow.nodes[0].pos = { 0: 3, 1: 4 };
parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow));
expect(parsedWorkflow.nodes[0].pos).toEqual([3, 4]);
});
it("workflow.nodes.widget_values", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.nodes[0].widgets_values = ["foo", "bar"];
await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow();
it("workflow.nodes.widget_values", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.nodes[0].widgets_values = ["foo", "bar"];
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
workflow.nodes[0].widgets_values = "foo";
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow();
workflow.nodes[0].widgets_values = "foo";
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
workflow.nodes[0].widgets_values = undefined;
await expect(parseComfyWorkflow(JSON.stringify(workflow))).resolves.not.toThrow();
workflow.nodes[0].widgets_values = undefined;
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
// The object format of widgets_values is used by VHS nodes to perform
// dynamic widgets display.
workflow.nodes[0].widgets_values = {"foo": "bar"};
const parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow));
expect(parsedWorkflow.nodes[0].widgets_values).toEqual({"foo": "bar"});
});
// The object format of widgets_values is used by VHS nodes to perform
// dynamic widgets display.
workflow.nodes[0].widgets_values = { foo: "bar" };
const parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow));
expect(parsedWorkflow.nodes[0].widgets_values).toEqual({ foo: "bar" });
});
});

View File

@@ -49,7 +49,9 @@ describe("extensions", () => {
// 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);
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];
@@ -71,15 +73,23 @@ describe("extensions", () => {
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);
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);
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);
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);
expect(mockExtension.loadedGraphNode.mock.calls[i][0].type).toBe(
graphData.nodes[i].type
);
}
// After configure is then called once all the setup is done
@@ -104,15 +114,21 @@ describe("extensions", () => {
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]);
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.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
@@ -123,13 +139,19 @@ describe("extensions", () => {
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount);
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.nodeCreated).toHaveBeenCalledTimes(
graphData.nodes.length + 2
);
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(
graphData.nodes.length + 1
);
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(2);
}, 15000);

View File

@@ -1,4 +1,9 @@
import { start, createDefaultWorkflow, getNodeDef, checkBeforeAndAfterReload } from "../utils";
import {
start,
createDefaultWorkflow,
getNodeDef,
checkBeforeAndAfterReload,
} from "../utils";
import { EzNode } from "../utils/ezgraph";
import lg from "../utils/litegraph";
@@ -25,9 +30,9 @@ describe("group node", () => {
n.select(true);
}
expect(Object.keys(app.canvas.selected_nodes).sort((a, b) => +a - +b)).toEqual(
nodes.map((n) => n.id + "").sort((a, b) => +a - +b)
);
expect(
Object.keys(app.canvas.selected_nodes).sort((a, b) => +a - +b)
).toEqual(nodes.map((n) => n.id + "").sort((a, b) => +a - +b));
global.prompt = jest.fn().mockImplementation(() => name);
const groupNode = await nodes[0].menu["Convert to Group Node"].call(false);
@@ -57,10 +62,22 @@ describe("group node", () => {
}, {});
}
const expected = {
1: { inputs: { ckpt_name: "model1.safetensors", ...valueMap?.[1] }, class_type: "CheckpointLoaderSimple" },
2: { inputs: { text: "positive", clip: ["1", 1], ...valueMap?.[2] }, class_type: "CLIPTextEncode" },
3: { inputs: { text: "negative", clip: ["1", 1], ...valueMap?.[3] }, class_type: "CLIPTextEncode" },
4: { inputs: { width: 512, height: 512, batch_size: 1, ...valueMap?.[4] }, class_type: "EmptyLatentImage" },
1: {
inputs: { ckpt_name: "model1.safetensors", ...valueMap?.[1] },
class_type: "CheckpointLoaderSimple",
},
2: {
inputs: { text: "positive", clip: ["1", 1], ...valueMap?.[2] },
class_type: "CLIPTextEncode",
},
3: {
inputs: { text: "negative", clip: ["1", 1], ...valueMap?.[3] },
class_type: "CLIPTextEncode",
},
4: {
inputs: { width: 512, height: 512, batch_size: 1, ...valueMap?.[4] },
class_type: "EmptyLatentImage",
},
5: {
inputs: {
seed: 0,
@@ -77,8 +94,18 @@ describe("group node", () => {
},
class_type: "KSampler",
},
6: { inputs: { samples: ["5", 0], vae: ["1", 2], ...valueMap?.[6] }, class_type: "VAEDecode" },
7: { inputs: { filename_prefix: "ComfyUI", images: ["6", 0], ...valueMap?.[7] }, class_type: "SaveImage" },
6: {
inputs: { samples: ["5", 0], vae: ["1", 2], ...valueMap?.[6] },
class_type: "VAEDecode",
},
7: {
inputs: {
filename_prefix: "ComfyUI",
images: ["6", 0],
...valueMap?.[7],
},
class_type: "SaveImage",
},
};
// Map old IDs to new at the top level
@@ -107,33 +134,58 @@ describe("group node", () => {
test("can be created from selected nodes", async () => {
const { ez, graph, app } = await start();
const nodes = createDefaultWorkflow(ez, graph);
const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg, nodes.empty]);
const group = await convertToGroup(app, graph, "test", [
nodes.pos,
nodes.neg,
nodes.empty,
]);
// Ensure links are now to the group node
expect(group.inputs).toHaveLength(2);
expect(group.outputs).toHaveLength(3);
expect(group.inputs.map((i) => i.input.name)).toEqual(["clip", "CLIPTextEncode clip"]);
expect(group.outputs.map((i) => i.output.name)).toEqual(["LATENT", "CONDITIONING", "CLIPTextEncode CONDITIONING"]);
expect(group.inputs.map((i) => i.input.name)).toEqual([
"clip",
"CLIPTextEncode clip",
]);
expect(group.outputs.map((i) => i.output.name)).toEqual([
"LATENT",
"CONDITIONING",
"CLIPTextEncode CONDITIONING",
]);
// ckpt clip to both clip inputs on the group
expect(nodes.ckpt.outputs.CLIP.connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
expect(
nodes.ckpt.outputs.CLIP.connections.map((t) => [
t.targetNode.id,
t.targetInput.index,
])
).toEqual([
[group.id, 0],
[group.id, 1],
]);
// group conditioning to sampler
expect(group.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
[nodes.sampler.id, 1],
]);
expect(
group.outputs["CONDITIONING"].connections.map((t) => [
t.targetNode.id,
t.targetInput.index,
])
).toEqual([[nodes.sampler.id, 1]]);
// group conditioning 2 to sampler
expect(
group.outputs["CLIPTextEncode CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])
group.outputs["CLIPTextEncode CONDITIONING"].connections.map((t) => [
t.targetNode.id,
t.targetInput.index,
])
).toEqual([[nodes.sampler.id, 2]]);
// group latent to sampler
expect(group.outputs["LATENT"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
[nodes.sampler.id, 3],
]);
expect(
group.outputs["LATENT"].connections.map((t) => [
t.targetNode.id,
t.targetInput.index,
])
).toEqual([[nodes.sampler.id, 3]]);
});
test("maintains all output links on conversion", async () => {
@@ -142,7 +194,10 @@ describe("group node", () => {
const save2 = ez.SaveImage(...nodes.decode.outputs);
const save3 = ez.SaveImage(...nodes.decode.outputs);
// Ensure an output with multiple links maintains them on convert to group
const group = await convertToGroup(app, graph, "test", [nodes.sampler, nodes.decode]);
const group = await convertToGroup(app, graph, "test", [
nodes.sampler,
nodes.decode,
]);
expect(group.outputs[0].connections.length).toBe(3);
expect(group.outputs[0].connections[0].targetNode.id).toBe(nodes.save.id);
expect(group.outputs[0].connections[1].targetNode.id).toBe(save2.id);
@@ -193,22 +248,36 @@ describe("group node", () => {
expect(sampler.widgets["control_after_generate"].value).toBe("fixed");
// validate links
expect(nodes.ckpt.outputs.CLIP.connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
expect(
nodes.ckpt.outputs.CLIP.connections.map((t) => [
t.targetNode.id,
t.targetInput.index,
])
).toEqual([
[pos.id, 0],
[neg.id, 0],
]);
expect(pos.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
[nodes.sampler.id, 1],
]);
expect(
pos.outputs["CONDITIONING"].connections.map((t) => [
t.targetNode.id,
t.targetInput.index,
])
).toEqual([[nodes.sampler.id, 1]]);
expect(neg.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
[nodes.sampler.id, 2],
]);
expect(
neg.outputs["CONDITIONING"].connections.map((t) => [
t.targetNode.id,
t.targetInput.index,
])
).toEqual([[nodes.sampler.id, 2]]);
expect(empty.outputs["LATENT"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([
[nodes.sampler.id, 3],
]);
expect(
empty.outputs["LATENT"].connections.map((t) => [
t.targetNode.id,
t.targetInput.index,
])
).toEqual([[nodes.sampler.id, 3]]);
});
test("it can embed reroutes as inputs", async () => {
const { ez, graph, app } = await start();
@@ -221,7 +290,12 @@ describe("group node", () => {
reroute.outputs[0].connectTo(nodes.neg.inputs[0]);
// Convert to group and ensure we only have 1 input of the correct type
const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg, nodes.empty, reroute]);
const group = await convertToGroup(app, graph, "test", [
nodes.pos,
nodes.neg,
nodes.empty,
reroute,
]);
expect(group.inputs).toHaveLength(1);
expect(group.inputs[0].input.type).toEqual("CLIP");
@@ -236,10 +310,16 @@ describe("group node", () => {
nodes.decode.outputs.IMAGE.connectTo(reroute.inputs[0]);
// Convert to group and ensure there is an IMAGE output
const group = await convertToGroup(app, graph, "test", [nodes.decode, nodes.save, reroute]);
const group = await convertToGroup(app, graph, "test", [
nodes.decode,
nodes.save,
reroute,
]);
expect(group.outputs).toHaveLength(1);
expect(group.outputs[0].output.type).toEqual("IMAGE");
expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.decode.id, nodes.save.id]));
expect((await graph.toPrompt()).output).toEqual(
getOutput([nodes.decode.id, nodes.save.id])
);
});
test("it can embed reroutes as pipes", async () => {
const { ez, graph, app } = await start();
@@ -253,13 +333,25 @@ describe("group node", () => {
nodes.ckpt.outputs.CLIP.connectTo(rerouteClip.inputs[0]);
nodes.ckpt.outputs.VAE.connectTo(rerouteVae.inputs[0]);
const group = await convertToGroup(app, graph, "test", [rerouteModel, rerouteClip, rerouteVae]);
const group = await convertToGroup(app, graph, "test", [
rerouteModel,
rerouteClip,
rerouteVae,
]);
expect(group.outputs).toHaveLength(3);
expect(group.outputs.map((o) => o.output.type)).toEqual(["MODEL", "CLIP", "VAE"]);
expect(group.outputs.map((o) => o.output.type)).toEqual([
"MODEL",
"CLIP",
"VAE",
]);
expect(group.outputs).toHaveLength(3);
expect(group.outputs.map((o) => o.output.type)).toEqual(["MODEL", "CLIP", "VAE"]);
expect(group.outputs.map((o) => o.output.type)).toEqual([
"MODEL",
"CLIP",
"VAE",
]);
group.outputs[0].connectTo(nodes.sampler.inputs.model);
group.outputs[1].connectTo(nodes.pos.inputs.clip);
@@ -279,7 +371,10 @@ describe("group node", () => {
}
prevNode.outputs[0].connectTo(nodes.sampler.inputs.model);
const group = await convertToGroup(app, graph, "test", [...reroutes, ...Object.values(nodes)]);
const group = await convertToGroup(app, graph, "test", [
...reroutes,
...Object.values(nodes),
]);
expect((await graph.toPrompt()).output).toEqual(getOutput());
group.menu["Convert to nodes"].call();
@@ -324,26 +419,38 @@ describe("group node", () => {
expect(group.widgets["denoise"].value).toEqual(0.9);
expect((await graph.toPrompt()).output).toEqual(
getOutput([nodes.ckpt.id, nodes.pos.id, nodes.neg.id, nodes.empty.id, nodes.sampler.id], {
[nodes.ckpt.id]: { ckpt_name: "model2.ckpt" },
[nodes.pos.id]: { text: "hello" },
[nodes.neg.id]: { text: "world" },
[nodes.empty.id]: { width: 256, height: 1024 },
[nodes.sampler.id]: {
seed: 1,
steps: 8,
cfg: 4.5,
sampler_name: "uni_pc",
scheduler: "karras",
denoise: 0.9,
},
})
getOutput(
[
nodes.ckpt.id,
nodes.pos.id,
nodes.neg.id,
nodes.empty.id,
nodes.sampler.id,
],
{
[nodes.ckpt.id]: { ckpt_name: "model2.ckpt" },
[nodes.pos.id]: { text: "hello" },
[nodes.neg.id]: { text: "world" },
[nodes.empty.id]: { width: 256, height: 1024 },
[nodes.sampler.id]: {
seed: 1,
steps: 8,
cfg: 4.5,
sampler_name: "uni_pc",
scheduler: "karras",
denoise: 0.9,
},
}
)
);
});
test("group inputs can be reroutes", async () => {
const { ez, graph, app } = await start();
const nodes = createDefaultWorkflow(ez, graph);
const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
const group = await convertToGroup(app, graph, "test", [
nodes.pos,
nodes.neg,
]);
const reroute = ez.Reroute();
nodes.ckpt.outputs.CLIP.connectTo(reroute.inputs[0]);
@@ -351,12 +458,17 @@ describe("group node", () => {
reroute.outputs[0].connectTo(group.inputs[0]);
reroute.outputs[0].connectTo(group.inputs[1]);
expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.pos.id, nodes.neg.id]));
expect((await graph.toPrompt()).output).toEqual(
getOutput([nodes.pos.id, nodes.neg.id])
);
});
test("group outputs can be reroutes", async () => {
const { ez, graph, app } = await start();
const nodes = createDefaultWorkflow(ez, graph);
const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
const group = await convertToGroup(app, graph, "test", [
nodes.pos,
nodes.neg,
]);
const reroute1 = ez.Reroute();
const reroute2 = ez.Reroute();
@@ -366,13 +478,21 @@ describe("group node", () => {
reroute1.outputs[0].connectTo(nodes.sampler.inputs.positive);
reroute2.outputs[0].connectTo(nodes.sampler.inputs.negative);
expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.pos.id, nodes.neg.id]));
expect((await graph.toPrompt()).output).toEqual(
getOutput([nodes.pos.id, nodes.neg.id])
);
});
test("groups can connect to each other", async () => {
const { ez, graph, app } = await start();
const nodes = createDefaultWorkflow(ez, graph);
const group1 = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
const group2 = await convertToGroup(app, graph, "test2", [nodes.empty, nodes.sampler]);
const group1 = await convertToGroup(app, graph, "test", [
nodes.pos,
nodes.neg,
]);
const group2 = await convertToGroup(app, graph, "test2", [
nodes.empty,
nodes.sampler,
]);
group1.outputs[0].connectTo(group2.inputs["positive"]);
group1.outputs[1].connectTo(group2.inputs["negative"]);
@@ -392,7 +512,10 @@ describe("group node", () => {
latent.outputs[0].connectTo(latentReroute.inputs[0]);
vae.outputs[0].connectTo(vaeReroute.inputs[0]);
const group1 = await convertToGroup(app, graph, "test", [latentReroute, vaeReroute]);
const group1 = await convertToGroup(app, graph, "test", [
latentReroute,
vaeReroute,
]);
group1.menu.Clone.call();
expect(app.graph._nodes).toHaveLength(4);
const group2 = graph.find(app.graph._nodes[3]);
@@ -406,10 +529,22 @@ describe("group node", () => {
const preview = ez.PreviewImage(decode.outputs[0]);
const output = {
[latent.id]: { inputs: { width: 512, height: 512, batch_size: 1 }, class_type: "EmptyLatentImage" },
[vae.id]: { inputs: { vae_name: "vae1.safetensors" }, class_type: "VAELoader" },
[decode.id]: { inputs: { samples: [latent.id + "", 0], vae: [vae.id + "", 0] }, class_type: "VAEDecode" },
[preview.id]: { inputs: { images: [decode.id + "", 0] }, class_type: "PreviewImage" },
[latent.id]: {
inputs: { width: 512, height: 512, batch_size: 1 },
class_type: "EmptyLatentImage",
},
[vae.id]: {
inputs: { vae_name: "vae1.safetensors" },
class_type: "VAELoader",
},
[decode.id]: {
inputs: { samples: [latent.id + "", 0], vae: [vae.id + "", 0] },
class_type: "VAEDecode",
},
[preview.id]: {
inputs: { images: [decode.id + "", 0] },
class_type: "PreviewImage",
},
};
expect((await graph.toPrompt()).output).toEqual(output);
@@ -433,7 +568,9 @@ describe("group node", () => {
const { api } = await import("../../src/scripts/api");
api.dispatchEvent(new CustomEvent("execution_start", {}));
api.dispatchEvent(new CustomEvent("executing", { detail: `${nodes.save.id}` }));
api.dispatchEvent(
new CustomEvent("executing", { detail: `${nodes.save.id}` })
);
// Event should be forwarded to group node id
expect(+app.runningNodeId).toEqual(group.id);
expect(group.node["imgs"]).toBeFalsy();
@@ -473,7 +610,9 @@ describe("group node", () => {
// Check it works for internal node ids
api.dispatchEvent(new CustomEvent("execution_start", {}));
api.dispatchEvent(new CustomEvent("executing", { detail: `${group.id}:5` }));
api.dispatchEvent(
new CustomEvent("executing", { detail: `${group.id}:5` })
);
// Event should be forwarded to group node id
expect(+app.runningNodeId).toEqual(group.id);
expect(group.node["imgs"]).toBeFalsy();
@@ -506,7 +645,10 @@ describe("group node", () => {
test("allows widgets to be converted to inputs", async () => {
const { ez, graph, app } = await start();
const nodes = createDefaultWorkflow(ez, graph);
const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
const group = await convertToGroup(app, graph, "test", [
nodes.pos,
nodes.neg,
]);
group.widgets[0].convertToInput();
const primitive = ez.PrimitiveNode();
@@ -555,11 +697,21 @@ describe("group node", () => {
let i = 0;
expect((await graph.toPrompt()).output).toEqual({
...getOutput([nodes.empty.id, nodes.pos.id, nodes.neg.id, nodes.sampler.id, nodes.decode.id, nodes.save.id], {
[nodes.empty.id]: { width: 256 },
[nodes.pos.id]: { text: "hello" },
[nodes.sampler.id]: { seed: 1 },
}),
...getOutput(
[
nodes.empty.id,
nodes.pos.id,
nodes.neg.id,
nodes.sampler.id,
nodes.decode.id,
nodes.save.id,
],
{
[nodes.empty.id]: { width: 256 },
[nodes.pos.id]: { text: "hello" },
[nodes.sampler.id]: { seed: 1 },
}
),
...getOutput(
{
[nodes.empty.id]: `${group2.id}:${i++}`,
@@ -582,7 +734,10 @@ describe("group node", () => {
test("is embedded in workflow", async () => {
let { ez, graph, app } = await start();
const nodes = createDefaultWorkflow(ez, graph);
let group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]);
let group = await convertToGroup(app, graph, "test", [
nodes.pos,
nodes.neg,
]);
const workflow = JSON.stringify((await graph.toPrompt()).workflow);
// Clear the environment
@@ -659,7 +814,11 @@ describe("group node", () => {
primitive.outputs[0].connectTo(pos.inputs.text);
primitive.outputs[0].connectTo(neg.inputs.text);
const group = await convertToGroup(app, graph, "test", [pos, neg, primitive]);
const group = await convertToGroup(app, graph, "test", [
pos,
neg,
primitive,
]);
// This will use a primitive widget named 'value'
expect(group.widgets.length).toBe(1);
expect(group.widgets["value"].value).toBe("positive");
@@ -680,7 +839,9 @@ describe("group node", () => {
});
test("correctly handles widget inputs", async () => {
const { ez, graph, app } = await start();
const upscaleMethods = (await getNodeDef("ImageScaleBy")).input.required["upscale_method"][0];
const upscaleMethods = (await getNodeDef("ImageScaleBy")).input.required[
"upscale_method"
][0];
const image = ez.LoadImage();
const scale1 = ez.ImageScaleBy(image.outputs[0]);
@@ -707,23 +868,40 @@ describe("group node", () => {
expect(primitive.widgets.value.widget.options.values).toBe(upscaleMethods);
expect(primitive.widgets.value.value).toBe(upscaleMethods[1]); // Ensure value is copied
primitive.widgets.value.value = upscaleMethods[1];
await checkBeforeAndAfterReload(graph, async (r) => {
const scale1id = r ? `${group.id}:0` : scale1.id;
const scale2id = r ? `${group.id}:1` : scale2.id;
// Ensure widget value is applied to prompt
expect((await graph.toPrompt()).output).toStrictEqual({
[image.id]: { inputs: { image: "example.png", upload: "image" }, class_type: "LoadImage" },
[image.id]: {
inputs: { image: "example.png", upload: "image" },
class_type: "LoadImage",
},
[scale1id]: {
inputs: { upscale_method: upscaleMethods[1], scale_by: 1, image: [`${image.id}`, 0] },
inputs: {
upscale_method: upscaleMethods[1],
scale_by: 1,
image: [`${image.id}`, 0],
},
class_type: "ImageScaleBy",
},
[scale2id]: {
inputs: { upscale_method: "nearest-exact", scale_by: 1, image: [`${image.id}`, 0] },
inputs: {
upscale_method: "nearest-exact",
scale_by: 1,
image: [`${image.id}`, 0],
},
class_type: "ImageScaleBy",
},
[preview1.id]: { inputs: { images: [`${scale1id}`, 0] }, class_type: "PreviewImage" },
[preview2.id]: { inputs: { images: [`${scale2id}`, 0] }, class_type: "PreviewImage" },
[preview1.id]: {
inputs: { images: [`${scale1id}`, 0] },
class_type: "PreviewImage",
},
[preview2.id]: {
inputs: { images: [`${scale2id}`, 0] },
class_type: "PreviewImage",
},
});
});
});
@@ -738,7 +916,12 @@ describe("group node", () => {
decode.outputs.IMAGE.connectTo(save.inputs.images);
empty.outputs.LATENT.connectTo(scale.inputs.samples);
const group = await convertToGroup(app, graph, "test", [scale, save, empty, decode]);
const group = await convertToGroup(app, graph, "test", [
scale,
save,
empty,
decode,
]);
const widgets = group.widgets.map((w) => w.widget.name);
expect(widgets).toStrictEqual([
"width",
@@ -758,7 +941,11 @@ describe("group node", () => {
const preview1 = ez.PreviewImage(...decode.outputs);
const preview2 = ez.PreviewImage(...decode.outputs);
const group = await convertToGroup(app, graph, "test", [img, decode, preview1]);
const group = await convertToGroup(app, graph, "test", [
img,
decode,
preview1,
]);
// Ensure we have an output connected to the 2nd preview node
expect(group.outputs.length).toBe(1);
@@ -792,7 +979,12 @@ describe("group node", () => {
const preview = ez.PreviewImage(decode2.outputs.IMAGE);
vae.outputs.VAE.connectTo(encode.inputs.vae);
const group = await convertToGroup(app, graph, "test", [vae, decode1, encode, sampler]);
const group = await convertToGroup(app, graph, "test", [
vae,
decode1,
encode,
sampler,
]);
expect(group.outputs.length).toBe(3);
expect(group.outputs[0].output.name).toBe("VAE");
@@ -815,11 +1007,31 @@ describe("group node", () => {
expect(group.outputs[2].connections[0].targetInput.index).toBe(0);
expect((await graph.toPrompt()).output).toEqual({
...getOutput({ 1: ckpt.id, 2: pos.id, 3: neg.id, 4: empty.id, 5: sampler.id, 6: decode1.id, 7: save.id }),
[vae.id]: { inputs: { vae_name: "vae1.safetensors" }, class_type: vae.node.type },
[encode.id]: { inputs: { pixels: ["6", 0], vae: [vae.id + "", 0] }, class_type: encode.node.type },
[decode2.id]: { inputs: { samples: [encode.id + "", 0], vae: [vae.id + "", 0] }, class_type: decode2.node.type },
[preview.id]: { inputs: { images: [decode2.id + "", 0] }, class_type: preview.node.type },
...getOutput({
1: ckpt.id,
2: pos.id,
3: neg.id,
4: empty.id,
5: sampler.id,
6: decode1.id,
7: save.id,
}),
[vae.id]: {
inputs: { vae_name: "vae1.safetensors" },
class_type: vae.node.type,
},
[encode.id]: {
inputs: { pixels: ["6", 0], vae: [vae.id + "", 0] },
class_type: encode.node.type,
},
[decode2.id]: {
inputs: { samples: [encode.id + "", 0], vae: [vae.id + "", 0] },
class_type: decode2.node.type,
},
[preview.id]: {
inputs: { images: [decode2.id + "", 0] },
class_type: preview.node.type,
},
});
});
test("works with IMAGEUPLOAD widget", async () => {
@@ -847,7 +1059,12 @@ describe("group node", () => {
primitive.outputs[0].connectTo(scale1.inputs.width);
primitive.outputs[0].connectTo(scale2.inputs.height);
const group = await convertToGroup(app, graph, "test", [img, primitive, scale1, scale2]);
const group = await convertToGroup(app, graph, "test", [
img,
primitive,
scale1,
scale2,
]);
group.widgets.value.value = 100;
expect((await graph.toPrompt()).output).toEqual({
1: {
@@ -855,11 +1072,23 @@ describe("group node", () => {
class_type: "LoadImage",
},
2: {
inputs: { upscale_method: "nearest-exact", width: 100, height: 512, crop: "disabled", image: ["1", 0] },
inputs: {
upscale_method: "nearest-exact",
width: 100,
height: 512,
crop: "disabled",
image: ["1", 0],
},
class_type: "ImageScale",
},
3: {
inputs: { upscale_method: "nearest-exact", width: 512, height: 100, crop: "disabled", image: ["1", 0] },
inputs: {
upscale_method: "nearest-exact",
width: 512,
height: 100,
crop: "disabled",
image: ["1", 0],
},
class_type: "ImageScale",
},
4: { inputs: { images: ["2", 0] }, class_type: "PreviewImage" },
@@ -930,7 +1159,13 @@ describe("group node", () => {
r2.outputs[0].connectTo(latent.inputs.width);
r2.outputs[0].connectTo(scale.inputs.height);
const group = await convertToGroup(app, graph, "test", [r1, r2, latent, decode, scale]);
const group = await convertToGroup(app, graph, "test", [
r1,
r2,
latent,
decode,
scale,
]);
expect(group.inputs[0].input.type).toBe("VAE");
expect(group.inputs[1].input.type).toBe("INT");
@@ -958,11 +1193,26 @@ describe("group node", () => {
await checkBeforeAndAfterReload(graph, async (r) => {
const id = (v) => (r ? `${group.id}:` : "") + v;
expect((await graph.toPrompt()).output).toStrictEqual({
1: { inputs: { vae_name: "vae1.safetensors" }, class_type: "VAELoader" },
[id(2)]: { inputs: { width: 32, height: 16, batch_size: 16 }, class_type: "EmptyLatentImage" },
[id(3)]: { inputs: { samples: [id(2), 0], vae: ["1", 0] }, class_type: "VAEDecode" },
1: {
inputs: { vae_name: "vae1.safetensors" },
class_type: "VAELoader",
},
[id(2)]: {
inputs: { width: 32, height: 16, batch_size: 16 },
class_type: "EmptyLatentImage",
},
[id(3)]: {
inputs: { samples: [id(2), 0], vae: ["1", 0] },
class_type: "VAEDecode",
},
[id(4)]: {
inputs: { upscale_method: "nearest-exact", width: 16, height: 32, crop: "disabled", image: [id(3), 0] },
inputs: {
upscale_method: "nearest-exact",
width: 16,
height: 32,
crop: "disabled",
image: [id(3), 0],
},
class_type: "ImageScale",
},
5: { inputs: { images: [id(4), 0] }, class_type: "PreviewImage" },

View File

@@ -1,79 +1,55 @@
import { NodeSearchService } from "@/services/nodeSearchService";
import { ComfyNodeDef } from "@/types/apiTypes";
const EXAMPLE_NODE_DEFS: ComfyNodeDef[] = [{
"input": {
"required": {
"ckpt_name": [
[
"model1.safetensors",
"model2.ckpt"
]
]
}
const EXAMPLE_NODE_DEFS: ComfyNodeDef[] = [
{
input: {
required: {
ckpt_name: [["model1.safetensors", "model2.ckpt"]],
},
},
output: ["MODEL", "CLIP", "VAE"],
output_is_list: [false, false, false],
output_name: ["MODEL", "CLIP", "VAE"],
name: "CheckpointLoaderSimple",
display_name: "Load Checkpoint",
description: "",
python_module: "nodes",
category: "loaders",
output_node: false,
},
"output": [
"MODEL",
"CLIP",
"VAE"
],
"output_is_list": [
false,
false,
false
],
"output_name": [
"MODEL",
"CLIP",
"VAE"
],
"name": "CheckpointLoaderSimple",
"display_name": "Load Checkpoint",
"description": "",
"python_module": "nodes",
"category": "loaders",
"output_node": false,
},
{
"input": {
"required": {
"samples": [
"LATENT"
],
"batch_index": [
"INT",
{
"default": 0,
"min": 0,
"max": 63
}
],
"length": [
"INT",
{
"default": 1,
"min": 1,
"max": 64
}
]
}
{
input: {
required: {
samples: ["LATENT"],
batch_index: [
"INT",
{
default: 0,
min: 0,
max: 63,
},
],
length: [
"INT",
{
default: 1,
min: 1,
max: 64,
},
],
},
},
output: ["LATENT"],
output_is_list: [false],
output_name: ["LATENT"],
name: "LatentFromBatch",
display_name: "Latent From Batch",
description: "",
python_module: "nodes",
category: "latent/batch",
output_node: false,
},
"output": [
"LATENT"
],
"output_is_list": [
false
],
"output_name": [
"LATENT"
],
"name": "LatentFromBatch",
"display_name": "Latent From Batch",
"description": "",
"python_module": "nodes",
"category": "latent/batch",
"output_node": false
},
];
describe("nodeSearchService", () => {
@@ -83,4 +59,4 @@ describe("nodeSearchService", () => {
expect(service.searchNode("L", [[inputFilter, "LATENT"]])).toHaveLength(1);
expect(service.searchNode("L")).toHaveLength(2);
});
});
});

View File

@@ -26,21 +26,28 @@ describe("users", () => {
async function waitForUserScreenShow() {
// Wait for "show" to be called
const { UserSelectionScreen } = await import("../../src/scripts/ui/userSelection");
const { UserSelectionScreen } = await import(
"../../src/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;
});
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);
setTimeout(
() => reject("timeout waiting for UserSelectionScreen to be shown."),
500
);
await p;
await new Promise(process.nextTick); // wait for promises to resolve
}
@@ -80,7 +87,9 @@ describe("users", () => {
const s = await starting;
// Ensure login is removed
expect(document.querySelectorAll("#comfy-user-selection")).toHaveLength(0);
expect(document.querySelectorAll("#comfy-user-selection")).toHaveLength(
0
);
expect(window.getComputedStyle(menu)?.display).not.toBe("none");
// Ensure settings + templates are saved
@@ -89,7 +98,11 @@ describe("users", () => {
expect(api.storeSettings).toHaveBeenCalledTimes(+isCreate);
expect(api.storeUserData).toHaveBeenCalledTimes(+isCreate);
if (isCreate) {
expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false });
expect(api.storeUserData).toHaveBeenCalledWith(
"comfy.templates.json",
null,
{ stringify: false }
);
expect(s.app.isNewUserSession).toBeTruthy();
} else {
expect(s.app.isNewUserSession).toBeFalsy();
@@ -226,7 +239,11 @@ describe("users", () => {
const { api } = await import("../../src/scripts/api");
expect(api.storeSettings).toHaveBeenCalledTimes(1);
expect(api.storeUserData).toHaveBeenCalledTimes(1);
expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false });
expect(api.storeUserData).toHaveBeenCalledWith(
"comfy.templates.json",
null,
{ stringify: false }
);
expect(app.isNewUserSession).toBeTruthy();
});
it("doesnt show user creation if default user", async () => {

View File

@@ -1,4 +1,10 @@
import { start, makeNodeDef, checkBeforeAndAfterReload, assertNotNullOrUndefined, createDefaultWorkflow } from "../utils";
import {
start,
makeNodeDef,
checkBeforeAndAfterReload,
assertNotNullOrUndefined,
createDefaultWorkflow,
} from "../utils";
import lg from "../utils/litegraph";
/**
@@ -14,7 +20,13 @@ import lg from "../utils/litegraph";
* @param { number } controlWidgetCount
* @returns
*/
async function connectPrimitiveAndReload(ez, graph, input, widgetType, controlWidgetCount = 0) {
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);
@@ -69,7 +81,9 @@ describe("widget inputs", () => {
].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 ?? {}] }),
mockNodeDefs: makeNodeDef("TestNode", {
[c.name]: [c.type, c.opt ?? {}],
}),
});
// Create test node and convert to input
@@ -81,7 +95,13 @@ describe("widget inputs", () => {
expect(input).toBeTruthy();
// @ts-ignore : input is valid here
await connectPrimitiveAndReload(ez, graph, input, c.widget ?? c.name, c.control);
await connectPrimitiveAndReload(
ez,
graph,
input,
c.widget ?? c.name,
c.control
);
});
});
@@ -107,7 +127,13 @@ describe("widget inputs", () => {
n.widgets.ckpt_name.convertToInput();
expect(n.inputs.length).toEqual(inputCount + 1);
const primitive = await connectPrimitiveAndReload(ez, graph, n.inputs.ckpt_name, "combo", 2);
const primitive = await connectPrimitiveAndReload(
ez,
graph,
n.inputs.ckpt_name,
"combo",
2
);
// Disconnect & reconnect
primitive.outputs[0].connections[0].disconnect();
@@ -173,7 +199,15 @@ describe("widget inputs", () => {
flags: {},
order: 1,
mode: 0,
inputs: [{ name: "test", type: "FLOAT", link: 4, widget: { name: "test" }, slot_index: 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],
@@ -200,14 +234,18 @@ describe("widget inputs", () => {
expect(dialogShow).toBeCalledTimes(1);
// @ts-ignore
expect(dialogShow.mock.calls[0][0].innerHTML).toContain("the following node types were not found");
expect(dialogShow.mock.calls[0][0].innerHTML).toContain(
"the following node types were not found"
);
// @ts-ignore
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 }] }),
mockNodeDefs: makeNodeDef("TestNode", {
example: ["INT", { defaultInput: true }],
}),
});
// Create test node and ensure it starts as an input
@@ -246,7 +284,9 @@ describe("widget inputs", () => {
test("forceInput widgets can not be converted back to inputs", async () => {
const { graph, ez } = await start({
mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { forceInput: true }] }),
mockNodeDefs: makeNodeDef("TestNode", {
example: ["INT", { forceInput: true }],
}),
});
// Create test node and ensure it starts as an input
@@ -271,8 +311,12 @@ describe("widget inputs", () => {
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 }] }),
...makeNodeDef("TestNode1", {
example: [["A", "B", "C"], { forceInput: true }],
}),
...makeNodeDef("TestNode2", {
example: [["A", "B", "C"], { forceInput: true }],
}),
},
});
@@ -290,8 +334,12 @@ describe("widget inputs", () => {
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 }] }),
...makeNodeDef("TestNode1", {
example: [["A", "B", "C"], { forceInput: true }],
}),
...makeNodeDef("TestNode2", {
example: [["A", "B"], { forceInput: true }],
}),
},
});
@@ -307,8 +355,12 @@ describe("widget inputs", () => {
const { ez } = await start({
mockNodeDefs: {
...makeNodeDef("TestNode1", {}, [["A", "B"]]),
...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }),
...makeNodeDef("TestNode3", { example: [["A", "B", "C"], { forceInput: true }] }),
...makeNodeDef("TestNode2", {
example: [["A", "B"], { forceInput: true }],
}),
...makeNodeDef("TestNode3", {
example: [["A", "B", "C"], { forceInput: true }],
}),
},
});
@@ -323,7 +375,12 @@ describe("widget inputs", () => {
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"], {}] }),
...makeNodeDef("TestNode1", {
example: [
["A", "B", "C", "D", "AA", "BB", "CC", "DD", "AAA", "BBB"],
{},
],
}),
},
});
@@ -395,11 +452,24 @@ describe("widget inputs", () => {
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" },
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 },
inputs: {
width: values.width ?? 512,
height: values.height ?? 512,
batch_size: values?.batch_size ?? 1,
},
class_type: "EmptyLatentImage",
},
5: {
@@ -417,9 +487,15 @@ describe("widget inputs", () => {
},
class_type: "KSampler",
},
6: { inputs: { samples: ["5", 0], vae: ["1", 2] }, class_type: "VAEDecode" },
6: {
inputs: { samples: ["5", 0], vae: ["1", 2] },
class_type: "VAEDecode",
},
7: {
inputs: { filename_prefix: values.filename_prefix ?? "ComfyUI", images: ["6", 0] },
inputs: {
filename_prefix: values.filename_prefix ?? "ComfyUI",
images: ["6", 0],
},
class_type: "SaveImage",
},
});

View File

@@ -20,7 +20,10 @@ export class EzConnection {
link;
get originNode() {
return new EzNode(this.app, this.app.graph.getNodeById(this.link.origin_id));
return new EzNode(
this.app,
this.app.graph.getNodeById(this.link.origin_id)
);
}
get originOutput() {
@@ -28,7 +31,10 @@ export class EzConnection {
}
get targetNode() {
return new EzNode(this.app, this.app.graph.getNodeById(this.link.target_id));
return new EzNode(
this.app,
this.app.graph.getNodeById(this.link.target_id)
);
}
get targetInput() {
@@ -121,7 +127,11 @@ export class EzOutput extends EzSlot {
/**
* @type { LG["LLink"] | null }
*/
const link = this.node.node.connect(this.index, input.node.node, input.index);
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;
@@ -155,11 +165,21 @@ export class EzNodeMenuItem {
}
call(selectNode = true) {
if (!this.item?.callback) throw new Error(`Menu Item ${this.item?.content ?? "[null]"} has no callback.`);
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);
return this.item.callback.call(
this.node.node,
undefined,
undefined,
undefined,
undefined,
this.node.node
);
}
}
@@ -188,7 +208,7 @@ export class EzWidget {
set value(v) {
this.widget.value = v;
this.widget.callback?.call?.(this.widget, v)
this.widget.callback?.call?.(this.widget, v);
}
get isConvertedToInput() {
@@ -197,24 +217,35 @@ export class EzWidget {
}
getConvertedInput() {
if (!this.isConvertedToInput) throw new Error(`Widget ${this.widget.name} is not converted to input.`);
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);
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.`);
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`);
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.`);
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`);
var index = menu.findIndex(
(a) => a.content == `Convert ${this.widget.name} to input`
);
menu[index].callback.call();
}
}
@@ -251,7 +282,11 @@ export class EzNode {
}
get menu() {
return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem);
return this.#makeLookupArray(
() => this.app.canvas.getNodeMenuOptions(this.node),
"content",
EzNodeMenuItem
);
}
get isRemoved() {
@@ -287,25 +322,33 @@ export class EzNode {
* @returns { Record<string, InstanceType<T>> & Array<InstanceType<T>> }
*/
#makeLookupArray(nodeProperty, nameProperty, ctor) {
const items = typeof nodeProperty === "function" ? nodeProperty() : this.node[nodeProperty];
const items =
typeof nodeProperty === "function"
? nodeProperty()
: this.node[nodeProperty];
// @ts-ignore
return (items ?? []).reduce((p, s, i) => {
if (!s) return p;
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) {
const name = s[nameProperty];
const item = new ctor(this, i, s);
// @ts-ignore
if (name in p) {
throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`);
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 }));
// @ts-ignore
p[name] = item;
return p;
},
Object.assign([], { $: this })
);
}
}
@@ -388,28 +431,28 @@ export class EzGraph {
}
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 { boolean } clearGraph
* @param { LG["LiteGraph"] } LiteGraph
* @param { LG["LGraphCanvas"] } LGraphCanvas
* @returns { { graph: EzGraph, ez: Record<string, EzNodeFactory> } }
*/
graph(app, LiteGraph, LGraphCanvas, clearGraph = true) {
// Always set the active canvas so things work
LGraphCanvas.active_canvas = app.canvas;
/**
* 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 { boolean } clearGraph
* @param { LG["LiteGraph"] } LiteGraph
* @param { LG["LGraphCanvas"] } LGraphCanvas
* @returns { { graph: EzGraph, ez: Record<string, EzNodeFactory> } }
*/
graph(app, LiteGraph, LGraphCanvas, clearGraph = true) {
// Always set the active canvas so things work
LGraphCanvas.active_canvas = app.canvas;
if (clearGraph) {
app.graph.clear();

View File

@@ -4,7 +4,7 @@ import lg from "./litegraph";
import fs from "fs";
import path from "path";
const html = fs.readFileSync(path.resolve(__dirname, "../../index.html"))
const html = fs.readFileSync(path.resolve(__dirname, "../../index.html"));
interface StartConfig extends APIConfig {
resetEnv?: boolean;
@@ -20,18 +20,18 @@ interface StartResult {
/**
*
* @param { Parameters<typeof mockApi>[0] & {
* resetEnv?: boolean,
* @param { Parameters<typeof mockApi>[0] & {
* resetEnv?: boolean,
* preSetup?(app): Promise<void>,
* localStorage?: Record<string, string>
* localStorage?: Record<string, string>
* } } config
* @returns
*/
export async function start(config: StartConfig = {}): Promise<StartResult> {
if(config.resetEnv) {
if (config.resetEnv) {
jest.resetModules();
jest.resetAllMocks();
lg.setup(global);
lg.setup(global);
localStorage.clear();
sessionStorage.clear();
}
@@ -39,14 +39,14 @@ export async function start(config: StartConfig = {}): Promise<StartResult> {
Object.assign(localStorage, config.localStorage ?? {});
document.body.innerHTML = html.toString();
mockApi(config);
const { app } = await import("../../src/scripts/app");
const { LiteGraph, LGraphCanvas } = await import("@comfyorg/litegraph");
config.preSetup?.(app);
await app.setup();
mockApi(config);
const { app } = await import("../../src/scripts/app");
const { LiteGraph, LGraphCanvas } = await import("@comfyorg/litegraph");
config.preSetup?.(app);
await app.setup();
// @ts-ignore
return { ...Ez.graph(app, LiteGraph, LGraphCanvas), app };
// @ts-ignore
return { ...Ez.graph(app, LiteGraph, LGraphCanvas), app };
}
/**
@@ -77,7 +77,8 @@ export function makeNodeDef(name, input, output = {}) {
},
};
for (const k in input) {
nodeDef.input.required[k] = typeof input[k] === "string" ? [input[k], {}] : [...input[k]];
nodeDef.input.required[k] =
typeof input[k] === "string" ? [input[k], {}] : [...input[k]];
}
if (output instanceof Array) {
output = output.reduce((p, c) => {
@@ -143,4 +144,4 @@ export async function getNodeDefs() {
export async function getNodeDef(nodeId) {
return (await getNodeDefs())[nodeId];
}
}

View File

@@ -18,14 +18,12 @@ function forEachKey(cb) {
}
export default {
setup(ctx) {
setup(ctx) {},
},
teardown(ctx) {
// forEachKey((k) => delete ctx[k]);
teardown(ctx) {
// forEachKey((k) => delete ctx[k]);
// Clear document after each run
document.getElementsByTagName("html")[0].innerHTML = "";
}
};
},
};

View File

@@ -17,7 +17,11 @@ export interface APIConfig {
mockExtensions?: string[];
mockNodeDefs?: Record<string, any>;
settings?: Record<string, string>;
userConfig?: { storage: "server" | "browser"; users?: Record<string, any>; migrated?: boolean };
userConfig?: {
storage: "server" | "browser";
users?: Record<string, any>;
migrated?: boolean;
};
userData?: Record<string, any>;
}
@@ -29,9 +33,9 @@ export interface APIConfig {
* @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>
* settings?: Record<string, string>
* userConfig?: {storage: "server" | "browser", users?: Record<string, any>, migrated?: boolean },
* userData?: Record<string, any>
* }} config
*/
export function mockApi(config: APIConfig = {}) {
@@ -46,7 +50,9 @@ export function mockApi(config: APIConfig = {}) {
.map((x) => path.relative(path.resolve("./src/"), x).replace(/\\/g, "/"));
}
if (!mockNodeDefs) {
mockNodeDefs = JSON.parse(fs.readFileSync(path.resolve("./tests-ui/data/object_info.json")));
mockNodeDefs = JSON.parse(
fs.readFileSync(path.resolve("./tests-ui/data/object_info.json"))
);
}
const events = new EventTarget();
@@ -62,14 +68,16 @@ export function mockApi(config: APIConfig = {}) {
fileURL: jest.fn((x) => "src/" + x),
createUser: jest.fn((username) => {
// @ts-ignore
if(username in userConfig.users) {
return { status: 400, json: () => "Duplicate" }
if (username in userConfig.users) {
return { status: 400, json: () => "Duplicate" };
}
// @ts-ignore
userConfig.users[username + "!"] = username;
return { status: 200, json: () => username + "!" }
return { status: 200, json: () => username + "!" };
}),
getUserConfig: jest.fn(() => userConfig ?? { storage: "browser", migrated: false }),
getUserConfig: jest.fn(
() => userConfig ?? { storage: "browser", migrated: false }
),
getSettings: jest.fn(() => settings),
storeSettings: jest.fn((v) => Object.assign(settings, v)),
getUserData: jest.fn((f) => {