mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Format everything (#211)
This commit is contained in:
@@ -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 }>({
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@ export default {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: [],
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = "";
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user