From 1b7db43f8a91a09cbcfc9bd7797c5e056c2e7cf3 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Tue, 23 Jul 2024 15:40:54 -0400 Subject: [PATCH] Format everything (#211) --- browser_tests/ComfyPage.ts | 71 +++- browser_tests/interaction.spec.ts | 66 ++-- browser_tests/litegraphEvent.spec.ts | 4 +- browser_tests/nodeSearchBox.spec.ts | 3 +- browser_tests/rightClickMenu.spec.ts | 150 ++++---- jest.config.ts | 39 +- package.json | 4 +- playwright.config.ts | 12 +- postcss.config.js | 2 +- scripts/deploy.js | 8 +- scripts/zipdist.js | 6 +- src/scripts/changeTracker.ts | 2 +- src/scripts/workflows.ts | 12 +- tailwind.config.js | 8 +- tests-ui/setup.ts | 16 +- tests-ui/tests/comfyWorkflow.test.ts | 158 ++++---- tests-ui/tests/extensions.test.ts | 44 ++- tests-ui/tests/groupNode.test.ts | 444 ++++++++++++++++++----- tests-ui/tests/nodeSearchService.test.ts | 120 +++--- tests-ui/tests/users.test.ts | 39 +- tests-ui/tests/widgetInputs.test.ts | 120 ++++-- tests-ui/utils/ezgraph.ts | 143 +++++--- tests-ui/utils/index.ts | 31 +- tests-ui/utils/litegraph.ts | 12 +- tests-ui/utils/setup.ts | 26 +- 25 files changed, 1014 insertions(+), 526 deletions(-) diff --git a/browser_tests/ComfyPage.ts b/browser_tests/ComfyPage.ts index 508f31df0..9542ce6e4 100644 --- a/browser_tests/ComfyPage.ts +++ b/browser_tests/ComfyPage.ts @@ -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 }>({ diff --git a/browser_tests/interaction.spec.ts b/browser_tests/interaction.spec.ts index f0a67f098..3895aca74 100644 --- a/browser_tests/interaction.spec.ts +++ b/browser_tests/interaction.spec.ts @@ -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"); }); }); diff --git a/browser_tests/litegraphEvent.spec.ts b/browser_tests/litegraphEvent.spec.ts index 8431e7907..8683abe35 100644 --- a/browser_tests/litegraphEvent.spec.ts +++ b/browser_tests/litegraphEvent.spec.ts @@ -3,7 +3,9 @@ import { comfyPageFixture as test } from "./ComfyPage"; function listenForEvent(): Promise { return new Promise((resolve) => { - document.addEventListener("litegraph:canvas", (e) => resolve(e), { once: true }); + document.addEventListener("litegraph:canvas", (e) => resolve(e), { + once: true, + }); }); } diff --git a/browser_tests/nodeSearchBox.spec.ts b/browser_tests/nodeSearchBox.spec.ts index 86a062527..8ee6aa573 100644 --- a/browser_tests/nodeSearchBox.spec.ts +++ b/browser_tests/nodeSearchBox.spec.ts @@ -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 }) => { diff --git a/browser_tests/rightClickMenu.spec.ts b/browser_tests/rightClickMenu.spec.ts index 309605bb2..6617f1aff 100644 --- a/browser_tests/rightClickMenu.spec.ts +++ b/browser_tests/rightClickMenu.spec.ts @@ -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" + ); + }); }); diff --git a/jest.config.ts b/jest.config.ts index dba31cae4..db7ad44f7 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -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: { - "^@/(.*)$": "/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: { + "^@/(.*)$": "/src/$1", + "\\.(css|less|scss|sass)$": "identity-obj-proxy", + }, }; -export default jestConfig; \ No newline at end of file +export default jestConfig; diff --git a/package.json b/package.json index 94d0bc158..5777f8b1d 100644 --- a/package.json +++ b/package.json @@ -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" ] diff --git a/playwright.config.ts b/playwright.config.ts index f3469e302..6256b1565 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -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, }, diff --git a/postcss.config.js b/postcss.config.js index 2e7af2b7f..2aa7205d4 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/scripts/deploy.js b/scripts/deploy.js index 0e1b176ba..6f97958f5 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -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); - }); \ No newline at end of file + console.error("Error copying directory:", err); + }); diff --git a/scripts/zipdist.js b/scripts/zipdist.js index 296dce888..3afb7c737 100644 --- a/scripts/zipdist.js +++ b/scripts/zipdist.js @@ -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.'); } -}); \ No newline at end of file +}); diff --git a/src/scripts/changeTracker.ts b/src/scripts/changeTracker.ts index ce4adbbb8..34877d263 100644 --- a/src/scripts/changeTracker.ts +++ b/src/scripts/changeTracker.ts @@ -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() { diff --git a/src/scripts/workflows.ts b/src/scripts/workflows.ts index e027f9987..d7c1f91eb 100644 --- a/src/scripts/workflows.ts +++ b/src/scripts/workflows.ts @@ -24,7 +24,10 @@ export class ComfyWorkflowManager extends EventTarget { workflowLookup: Record = {}; workflows: Array = []; openWorkflows: Array = []; - queuedPrompts: Record; }> = {}; + queuedPrompts: Record< + string, + { workflow?: ComfyWorkflow; nodes?: Record } + > = {}; 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); diff --git a/tailwind.config.js b/tailwind.config.js index a28ce7117..45ce1122b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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: [], -} - +}; diff --git a/tests-ui/setup.ts b/tests-ui/setup.ts index 1703234cf..577d75fc5 100644 --- a/tests-ui/setup.ts +++ b/tests-ui/setup.ts @@ -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(); \ No newline at end of file +setup(); diff --git a/tests-ui/tests/comfyWorkflow.test.ts b/tests-ui/tests/comfyWorkflow.test.ts index f42f343fb..5fddc3281 100644 --- a/tests-ui/tests/comfyWorkflow.test.ts +++ b/tests-ui/tests/comfyWorkflow.test.ts @@ -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" }); + }); }); diff --git a/tests-ui/tests/extensions.test.ts b/tests-ui/tests/extensions.test.ts index d12ac73cd..3f341d47f 100644 --- a/tests-ui/tests/extensions.test.ts +++ b/tests-ui/tests/extensions.test.ts @@ -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); diff --git a/tests-ui/tests/groupNode.test.ts b/tests-ui/tests/groupNode.test.ts index c01b8e2b4..7bd2fcd3e 100644 --- a/tests-ui/tests/groupNode.test.ts +++ b/tests-ui/tests/groupNode.test.ts @@ -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" }, diff --git a/tests-ui/tests/nodeSearchService.test.ts b/tests-ui/tests/nodeSearchService.test.ts index fecc77d9b..1bef61b2c 100644 --- a/tests-ui/tests/nodeSearchService.test.ts +++ b/tests-ui/tests/nodeSearchService.test.ts @@ -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); }); -}); \ No newline at end of file +}); diff --git a/tests-ui/tests/users.test.ts b/tests-ui/tests/users.test.ts index ce54a3528..caa6f486d 100644 --- a/tests-ui/tests/users.test.ts +++ b/tests-ui/tests/users.test.ts @@ -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 () => { diff --git a/tests-ui/tests/widgetInputs.test.ts b/tests-ui/tests/widgetInputs.test.ts index 336a1246b..0a33d38fd 100644 --- a/tests-ui/tests/widgetInputs.test.ts +++ b/tests-ui/tests/widgetInputs.test.ts @@ -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", }, }); diff --git a/tests-ui/utils/ezgraph.ts b/tests-ui/utils/ezgraph.ts index cedfb031f..e8ea6f33f 100644 --- a/tests-ui/utils/ezgraph.ts +++ b/tests-ui/utils/ezgraph.ts @@ -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> & Array> } */ #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 } } - */ - 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 } } + */ + graph(app, LiteGraph, LGraphCanvas, clearGraph = true) { + // Always set the active canvas so things work + LGraphCanvas.active_canvas = app.canvas; if (clearGraph) { app.graph.clear(); diff --git a/tests-ui/utils/index.ts b/tests-ui/utils/index.ts index b34addd66..60f65cf29 100644 --- a/tests-ui/utils/index.ts +++ b/tests-ui/utils/index.ts @@ -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[0] & { - * resetEnv?: boolean, + * @param { Parameters[0] & { + * resetEnv?: boolean, * preSetup?(app): Promise, - * localStorage?: Record + * localStorage?: Record * } } config * @returns */ export async function start(config: StartConfig = {}): Promise { - 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 { 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]; -} \ No newline at end of file +} diff --git a/tests-ui/utils/litegraph.ts b/tests-ui/utils/litegraph.ts index 5d95980d2..9c89ca574 100644 --- a/tests-ui/utils/litegraph.ts +++ b/tests-ui/utils/litegraph.ts @@ -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 = ""; - } -}; \ No newline at end of file + }, +}; diff --git a/tests-ui/utils/setup.ts b/tests-ui/utils/setup.ts index 37392958a..cfe5399ac 100644 --- a/tests-ui/utils/setup.ts +++ b/tests-ui/utils/setup.ts @@ -17,7 +17,11 @@ export interface APIConfig { mockExtensions?: string[]; mockNodeDefs?: Record; settings?: Record; - userConfig?: { storage: "server" | "browser"; users?: Record; migrated?: boolean }; + userConfig?: { + storage: "server" | "browser"; + users?: Record; + migrated?: boolean; + }; userData?: Record; } @@ -29,9 +33,9 @@ export interface APIConfig { * @param {{ * mockExtensions?: string[], * mockNodeDefs?: Record, -* settings?: Record -* userConfig?: {storage: "server" | "browser", users?: Record, migrated?: boolean }, -* userData?: Record + * settings?: Record + * userConfig?: {storage: "server" | "browser", users?: Record, migrated?: boolean }, + * userData?: Record * }} 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) => {