Apply new code format standard (#217)

This commit is contained in:
Chenlei Hu
2024-07-25 10:10:18 -04:00
committed by GitHub
parent 19c70d95d3
commit e179f75387
121 changed files with 11898 additions and 11983 deletions

View File

@@ -1,9 +1,9 @@
import { start } from "./utils";
import lg from "./utils/litegraph";
import { start } from './utils'
import lg from './utils/litegraph'
// Load things once per test file before to ensure its all warmed up for the tests
beforeAll(async () => {
lg.setup(global);
await start({ resetEnv: true });
lg.teardown(global);
});
lg.setup(global)
await start({ resetEnv: true })
lg.teardown(global)
})

View File

@@ -3,168 +3,168 @@
Requires the repo to be cloned to the tests-ui directory or specified via the EXAMPLE_REPO_PATH env var.
*/
import chalk from "chalk";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "node:url";
import { getFromPngBuffer } from "@/scripts/metadata/png";
import { getFromFlacBuffer } from "@/scripts/metadata/flac";
import dotenv from "dotenv";
dotenv.config();
import chalk from 'chalk'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'node:url'
import { getFromPngBuffer } from '@/scripts/metadata/png'
import { getFromFlacBuffer } from '@/scripts/metadata/flac'
import dotenv from 'dotenv'
dotenv.config()
const dirname = path.dirname(fileURLToPath(import.meta.url));
const dirname = path.dirname(fileURLToPath(import.meta.url))
const repoPath =
process.env.EXAMPLE_REPO_PATH || path.resolve(dirname, "ComfyUI_examples");
const workflowsPath = path.resolve(dirname, "workflows", "examples");
process.env.EXAMPLE_REPO_PATH || path.resolve(dirname, 'ComfyUI_examples')
const workflowsPath = path.resolve(dirname, 'workflows', 'examples')
if (!fs.existsSync(repoPath)) {
console.error(
`ComfyUI_examples repo not found. Please clone this to ${repoPath} or set the EXAMPLE_REPO_PATH env var (see .env_example) and re-run.`
);
)
}
if (!fs.existsSync(workflowsPath)) {
await fs.promises.mkdir(workflowsPath);
await fs.promises.mkdir(workflowsPath)
}
async function* getFiles(
dir: string,
...exts: string[]
): AsyncGenerator<string, void, void> {
const dirents = await fs.promises.readdir(dir, { withFileTypes: true });
const dirents = await fs.promises.readdir(dir, { withFileTypes: true })
for (const dirent of dirents) {
const res = path.resolve(dir, dirent.name);
const res = path.resolve(dir, dirent.name)
if (dirent.isDirectory()) {
yield* getFiles(res, ...exts);
yield* getFiles(res, ...exts)
} else if (exts.includes(path.extname(res))) {
yield res;
yield res
}
}
}
async function validateMetadata(metadata: Record<string, string>) {
const check = (prop: "prompt" | "workflow") => {
const v = metadata?.[prop];
if (!v) throw `${prop} not found in metadata`;
const check = (prop: 'prompt' | 'workflow') => {
const v = metadata?.[prop]
if (!v) throw `${prop} not found in metadata`
try {
JSON.parse(v);
JSON.parse(v)
} catch (error) {
throw `${prop} invalid json: ${error.message}`;
throw `${prop} invalid json: ${error.message}`
}
return v;
};
return v
}
return { prompt: check("prompt"), workflow: check("workflow") };
return { prompt: check('prompt'), workflow: check('workflow') }
}
async function hasExampleChanged(
existingFilePath: string,
exampleJson: string
) {
return exampleJson !== (await fs.promises.readFile(existingFilePath, "utf8"));
return exampleJson !== (await fs.promises.readFile(existingFilePath, 'utf8'))
}
// Example images to ignore as they don't contain workflows
const ignore = [
"unclip_sunset.png",
"unclip_mountains.png",
"inpaint_yosemite_inpaint_example.png",
"controlnet_shark_depthmap.png",
"controlnet_pose_worship.png",
"controlnet_pose_present.png",
"controlnet_input_scribble_example.png",
"controlnet_house_scribble.png",
];
'unclip_sunset.png',
'unclip_mountains.png',
'inpaint_yosemite_inpaint_example.png',
'controlnet_shark_depthmap.png',
'controlnet_pose_worship.png',
'controlnet_pose_present.png',
'controlnet_input_scribble_example.png',
'controlnet_house_scribble.png'
]
// Find all existing examples so we can check if any are removed/changed
const existing = new Set(
(await fs.promises.readdir(workflowsPath, { withFileTypes: true }))
.filter((d) => d.isFile())
.map((d) => path.resolve(workflowsPath, d.name))
);
)
const results = {
new: [],
changed: [],
unchanged: [],
missing: [],
failed: [],
};
failed: []
}
let total = 0;
for await (const file of getFiles(repoPath, ".png", ".flac")) {
let total = 0
for await (const file of getFiles(repoPath, '.png', '.flac')) {
const cleanedName = path
.relative(repoPath, file)
.replaceAll("/", "_")
.replaceAll("\\", "_");
.replaceAll('/', '_')
.replaceAll('\\', '_')
if (ignore.includes(cleanedName)) continue;
total++;
if (ignore.includes(cleanedName)) continue
total++
let metadata: { prompt: string; workflow: string };
let metadata: { prompt: string; workflow: string }
try {
const { buffer } = await fs.promises.readFile(file);
const { buffer } = await fs.promises.readFile(file)
switch (path.extname(file)) {
case ".png":
metadata = await validateMetadata(getFromPngBuffer(buffer));
break;
case ".flac":
metadata = await validateMetadata(getFromFlacBuffer(buffer));
break;
case '.png':
metadata = await validateMetadata(getFromPngBuffer(buffer))
break
case '.flac':
metadata = await validateMetadata(getFromFlacBuffer(buffer))
break
}
const outPath = path.resolve(workflowsPath, cleanedName + ".json");
const exampleJson = JSON.stringify(metadata);
const outPath = path.resolve(workflowsPath, cleanedName + '.json')
const exampleJson = JSON.stringify(metadata)
if (existing.has(outPath)) {
existing.delete(outPath);
existing.delete(outPath)
if (await hasExampleChanged(outPath, exampleJson)) {
results.changed.push(outPath);
results.changed.push(outPath)
} else {
// Unchanged, no point in re-saving
results.unchanged.push(outPath);
continue;
results.unchanged.push(outPath)
continue
}
} else {
results.new.push(outPath);
results.new.push(outPath)
}
await fs.promises.writeFile(outPath, exampleJson, "utf8");
await fs.promises.writeFile(outPath, exampleJson, 'utf8')
} catch (error) {
results.failed.push({ file, error });
results.failed.push({ file, error })
}
}
// Any workflows left in the existing set are now missing, these will want checking and manually removing
results.missing.push(...existing);
results.missing.push(...existing)
const c = (v: number, gt0: "red" | "yellow" | "green") =>
chalk[v > 0 ? gt0 : "gray"](v);
const c = (v: number, gt0: 'red' | 'yellow' | 'green') =>
chalk[v > 0 ? gt0 : 'gray'](v)
console.log(`Processed ${chalk.green(total)} examples`);
console.log(` ${chalk.gray(results.unchanged.length)} unchanged`);
console.log(` ${c(results.changed.length, "yellow")} changed`);
console.log(` ${c(results.new.length, "green")} new`);
console.log(` ${c(results.missing.length, "red")} missing`);
console.log(` ${c(results.failed.length, "red")} failed`);
console.log(`Processed ${chalk.green(total)} examples`)
console.log(` ${chalk.gray(results.unchanged.length)} unchanged`)
console.log(` ${c(results.changed.length, 'yellow')} changed`)
console.log(` ${c(results.new.length, 'green')} new`)
console.log(` ${c(results.missing.length, 'red')} missing`)
console.log(` ${c(results.failed.length, 'red')} failed`)
if (results.missing.length) {
console.log();
console.log()
console.log(
chalk.red(
"The following examples are missing and require manual reviewing & removal:"
'The following examples are missing and require manual reviewing & removal:'
)
);
)
for (const m of results.missing) {
console.log(m);
console.log(m)
}
}
if (results.failed.length) {
console.log();
console.log(chalk.red("The following examples failed to extract:"));
console.log()
console.log(chalk.red('The following examples failed to extract:'))
for (const m of results.failed) {
console.log(m.file);
console.error(m.error);
console.log();
console.log(m.file)
console.error(m.error)
console.log()
}
}

View File

@@ -3,12 +3,12 @@ module.exports = async function () {
observe() {}
unobserve() {}
disconnect() {}
};
}
const { nop } = require("./utils/nopProxy");
global.enableWebGLCanvas = nop;
const { nop } = require('./utils/nopProxy')
global.enableWebGLCanvas = nop
HTMLCanvasElement.prototype.getContext = nop;
HTMLCanvasElement.prototype.getContext = nop
localStorage["Comfy.Settings.Comfy.Logging.Enabled"] = "false";
};
localStorage['Comfy.Settings.Comfy.Logging.Enabled'] = 'false'
}

View File

@@ -1,46 +1,46 @@
import { resolve } from "path";
import { existsSync, mkdirSync, writeFileSync } from "fs";
import http from "http";
import { resolve } from 'path'
import { existsSync, mkdirSync, writeFileSync } from 'fs'
import http from 'http'
async function setup() {
await new Promise<void>((res, rej) => {
http
.get("http://127.0.0.1:8188/object_info", (resp) => {
let data = "";
resp.on("data", (chunk) => {
data += chunk;
});
resp.on("end", () => {
.get('http://127.0.0.1:8188/object_info', (resp) => {
let data = ''
resp.on('data', (chunk) => {
data += chunk
})
resp.on('end', () => {
// Modify the response data to add some checkpoints
const objectInfo = JSON.parse(data);
const objectInfo = JSON.parse(data)
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name[0] = [
"model1.safetensors",
"model2.ckpt",
];
'model1.safetensors',
'model2.ckpt'
]
objectInfo.VAELoader.input.required.vae_name[0] = [
"vae1.safetensors",
"vae2.ckpt",
];
'vae1.safetensors',
'vae2.ckpt'
]
data = JSON.stringify(objectInfo, undefined, "\t");
data = JSON.stringify(objectInfo, undefined, '\t')
const outDir = resolve("./tests-ui/data");
const outDir = resolve('./tests-ui/data')
if (!existsSync(outDir)) {
mkdirSync(outDir);
mkdirSync(outDir)
}
const outPath = resolve(outDir, "object_info.json");
const outPath = resolve(outDir, 'object_info.json')
console.log(
`Writing ${Object.keys(objectInfo).length} nodes to ${outPath}`
);
)
writeFileSync(outPath, data, {
encoding: "utf8",
});
res();
});
encoding: 'utf8'
})
res()
})
})
.on("error", rej);
});
.on('error', rej)
})
}
setup();
setup()

View File

@@ -1,77 +1,77 @@
import { ComfyNodeDef, validateComfyNodeDef } from "@/types/apiTypes";
const fs = require("fs");
const path = require("path");
import { ComfyNodeDef, validateComfyNodeDef } from '@/types/apiTypes'
const fs = require('fs')
const path = require('path')
const EXAMPLE_NODE_DEF: ComfyNodeDef = {
input: {
required: {
ckpt_name: [["model1.safetensors", "model2.ckpt"]],
},
ckpt_name: [['model1.safetensors', 'model2.ckpt']]
}
},
output: ["MODEL", "CLIP", "VAE"],
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_name: ['MODEL', 'CLIP', 'VAE'],
name: 'CheckpointLoaderSimple',
display_name: 'Load Checkpoint',
description: '',
python_module: 'nodes',
category: 'loaders',
output_node: false
}
describe("validateNodeDef", () => {
it("Should accept a valid node definition", () => {
expect(() => validateComfyNodeDef(EXAMPLE_NODE_DEF)).not.toThrow();
});
describe('validateNodeDef', () => {
it('Should accept a valid node definition', () => {
expect(() => validateComfyNodeDef(EXAMPLE_NODE_DEF)).not.toThrow()
})
describe.each([
[{ ckpt_name: "foo" }, ["foo", {}]],
[{ ckpt_name: ["foo"] }, ["foo", {}]],
[{ ckpt_name: ["foo", { default: 1 }] }, ["foo", { default: 1 }]],
[{ ckpt_name: 'foo' }, ['foo', {}]],
[{ ckpt_name: ['foo'] }, ['foo', {}]],
[{ ckpt_name: ['foo', { default: 1 }] }, ['foo', { default: 1 }]]
])(
"validateComfyNodeDef with various input spec formats",
'validateComfyNodeDef with various input spec formats',
(inputSpec, expected) => {
it(`should accept input spec format: ${JSON.stringify(inputSpec)}`, () => {
expect(
validateComfyNodeDef({
...EXAMPLE_NODE_DEF,
input: {
required: inputSpec,
},
required: inputSpec
}
}).input.required.ckpt_name
).toEqual(expected);
});
).toEqual(expected)
})
}
);
)
describe.each([
[{ ckpt_name: { "model1.safetensors": "foo" } }],
[{ ckpt_name: ["*", ""] }],
[{ ckpt_name: ["foo", { default: 1 }, { default: 2 }] }],
[{ ckpt_name: { 'model1.safetensors': 'foo' } }],
[{ ckpt_name: ['*', ''] }],
[{ ckpt_name: ['foo', { default: 1 }, { default: 2 }] }]
])(
"validateComfyNodeDef rejects with various input spec formats",
'validateComfyNodeDef rejects with various input spec formats',
(inputSpec) => {
it(`should accept input spec format: ${JSON.stringify(inputSpec)}`, () => {
expect(() =>
validateComfyNodeDef({
...EXAMPLE_NODE_DEF,
input: {
required: inputSpec,
},
required: inputSpec
}
})
).toThrow();
});
).toThrow()
})
}
);
)
it("Should accept all built-in node definitions", async () => {
it('Should accept all built-in node definitions', async () => {
const nodeDefs = Object.values(
JSON.parse(
fs.readFileSync(path.resolve("./tests-ui/data/object_info.json"))
fs.readFileSync(path.resolve('./tests-ui/data/object_info.json'))
)
);
)
nodeDefs.forEach((nodeDef) => {
expect(() => validateComfyNodeDef(nodeDef)).not.toThrow();
});
});
});
expect(() => validateComfyNodeDef(nodeDef)).not.toThrow()
})
})
})

View File

@@ -1,121 +1,109 @@
import { parseComfyWorkflow } from "../../src/types/comfyWorkflow";
import { defaultGraph } from "../../src/scripts/defaultGraph";
import fs from "fs";
import { parseComfyWorkflow } from '../../src/types/comfyWorkflow'
import { defaultGraph } from '../../src/scripts/defaultGraph'
import fs from 'fs'
const WORKFLOW_DIR = "tests-ui/workflows";
const WORKFLOW_DIR = 'tests-ui/workflows'
describe("parseComfyWorkflow", () => {
it("parses valid workflow", async () => {
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();
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;
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 = []
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
).resolves.not.toThrow()
})
workflow.nodes = null;
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
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
).resolves.not.toThrow()
})
workflow.nodes = [];
it('workflow.extra', async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph))
workflow.extra = undefined
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
});
).resolves.not.toThrow()
it("workflow.version", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.version = undefined;
workflow.extra = null
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
).resolves.not.toThrow()
workflow.version = "1.0.1"; // Invalid format.
workflow.extra = {}
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
).resolves.not.toThrow()
workflow.version = 1;
workflow.extra = { foo: 'bar' } // Should accept extra fields.
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
});
).resolves.not.toThrow()
})
it("workflow.extra", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.extra = undefined;
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.extra = null;
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();
});
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();
).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]);
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"];
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();
).resolves.not.toThrow()
workflow.nodes[0].widgets_values = "foo";
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))
).rejects.toThrow();
workflow.nodes[0].widgets_values = undefined;
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
).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" });
});
});
workflow.nodes[0].widgets_values = { foo: 'bar' }
const parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow))
expect(parsedWorkflow.nodes[0].widgets_values).toEqual({ foo: 'bar' })
})
})

View File

@@ -1,69 +1,69 @@
import { readdirSync, readFileSync } from "fs";
import lg from "../utils/litegraph";
import path from "path";
import { start } from "../utils";
import { readdirSync, readFileSync } from 'fs'
import lg from '../utils/litegraph'
import path from 'path'
import { start } from '../utils'
const WORKFLOW_DIR = "tests-ui/workflows/examples";
const WORKFLOW_DIR = 'tests-ui/workflows/examples'
// Resolve basic differences in old prompts
function fixLegacyPrompt(prompt: { inputs: any }) {
for (const n of Object.values(prompt)) {
const { inputs } = n;
const { inputs } = n
// Added inputs
if (n.class_type === "VAEEncodeForInpaint") {
if (n.inputs["grow_mask_by"] == null) n.inputs["grow_mask_by"] = 6;
} else if (n.class_type === "SDTurboScheduler") {
if (n.inputs["denoise"] == null) n.inputs["denoise"] = 1;
if (n.class_type === 'VAEEncodeForInpaint') {
if (n.inputs['grow_mask_by'] == null) n.inputs['grow_mask_by'] = 6
} else if (n.class_type === 'SDTurboScheduler') {
if (n.inputs['denoise'] == null) n.inputs['denoise'] = 1
}
// This has been renamed
if (inputs["choose file to upload"]) {
const v = inputs["choose file to upload"];
delete inputs["choose file to upload"];
inputs["upload"] = v;
if (inputs['choose file to upload']) {
const v = inputs['choose file to upload']
delete inputs['choose file to upload']
inputs['upload'] = v
}
delete n["is_changed"];
delete n['is_changed']
}
return prompt;
return prompt
}
describe("example workflows", () => {
describe('example workflows', () => {
beforeEach(() => {
lg.setup(global);
});
lg.setup(global)
})
afterEach(() => {
lg.teardown(global);
});
lg.teardown(global)
})
for (const file of readdirSync(WORKFLOW_DIR)) {
if (!file.endsWith(".json")) continue;
if (!file.endsWith('.json')) continue
const { workflow, prompt } = JSON.parse(
readFileSync(path.resolve(WORKFLOW_DIR, file), "utf8")
);
readFileSync(path.resolve(WORKFLOW_DIR, file), 'utf8')
)
let skip = false;
let parsedWorkflow;
let skip = false
let parsedWorkflow
try {
// Workflows with group nodes dont generate the same IDs as the examples
// they'll need recreating so skip them for now.
parsedWorkflow = JSON.parse(workflow);
skip = !!Object.keys(parsedWorkflow?.extra?.groupNodes ?? {}).length;
parsedWorkflow = JSON.parse(workflow)
skip = !!Object.keys(parsedWorkflow?.extra?.groupNodes ?? {}).length
} catch (error) {}
(skip ? test.skip : test)(
"correctly generates prompt json for " + file,
;(skip ? test.skip : test)(
'correctly generates prompt json for ' + file,
async () => {
if (!workflow || !prompt) throw new Error("Invalid example json");
if (!workflow || !prompt) throw new Error('Invalid example json')
const { app } = await start();
await app.loadGraphData(parsedWorkflow);
const { app } = await start()
await app.loadGraphData(parsedWorkflow)
const output = await app.graphToPrompt();
expect(output.output).toEqual(fixLegacyPrompt(JSON.parse(prompt)));
const output = await app.graphToPrompt()
expect(output.output).toEqual(fixLegacyPrompt(JSON.parse(prompt)))
}
);
)
}
});
})

View File

@@ -1,18 +1,18 @@
import { start } from "../utils";
import lg from "../utils/litegraph";
import { start } from '../utils'
import lg from '../utils/litegraph'
describe("extensions", () => {
describe('extensions', () => {
beforeEach(() => {
lg.setup(global);
});
lg.setup(global)
})
afterEach(() => {
lg.teardown(global);
});
lg.teardown(global)
})
it("calls each extension hook", async () => {
it('calls each extension hook', async () => {
const mockExtension = {
name: "TestExtension",
name: 'TestExtension',
init: jest.fn(),
setup: jest.fn(),
addCustomNodeDefs: jest.fn(),
@@ -22,195 +22,191 @@ describe("extensions", () => {
loadedGraphNode: jest.fn(),
nodeCreated: jest.fn(),
beforeConfigureGraph: jest.fn(),
afterConfigureGraph: jest.fn(),
};
afterConfigureGraph: jest.fn()
}
const { app, ez, graph } = await start({
async preSetup(app) {
app.registerExtension(mockExtension);
},
});
app.registerExtension(mockExtension)
}
})
// Basic initialisation hooks should be called once, with app
expect(mockExtension.init).toHaveBeenCalledTimes(1);
expect(mockExtension.init).toHaveBeenCalledWith(app);
expect(mockExtension.init).toHaveBeenCalledTimes(1)
expect(mockExtension.init).toHaveBeenCalledWith(app)
// Adding custom node defs should be passed the full list of nodes
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
expect(mockExtension.addCustomNodeDefs.mock.calls[0][1]).toStrictEqual(app);
const defs = mockExtension.addCustomNodeDefs.mock.calls[0][0];
expect(defs).toHaveProperty("KSampler");
expect(defs).toHaveProperty("LoadImage");
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1)
expect(mockExtension.addCustomNodeDefs.mock.calls[0][1]).toStrictEqual(app)
const defs = mockExtension.addCustomNodeDefs.mock.calls[0][0]
expect(defs).toHaveProperty('KSampler')
expect(defs).toHaveProperty('LoadImage')
// Get custom widgets is called once and should return new widget types
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
expect(mockExtension.getCustomWidgets).toHaveBeenCalledWith(app);
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1)
expect(mockExtension.getCustomWidgets).toHaveBeenCalledWith(app)
// Before register node def will be called once per node type
const nodeNames = Object.keys(defs);
const nodeCount = nodeNames.length;
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(
nodeCount
);
const nodeNames = Object.keys(defs)
const nodeCount = nodeNames.length
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount)
for (let i = 0; i < 10; i++) {
// It should be send the JS class and the original JSON definition
const nodeClass = mockExtension.beforeRegisterNodeDef.mock.calls[i][0];
const nodeDef = mockExtension.beforeRegisterNodeDef.mock.calls[i][1];
const nodeClass = mockExtension.beforeRegisterNodeDef.mock.calls[i][0]
const nodeDef = mockExtension.beforeRegisterNodeDef.mock.calls[i][1]
expect(nodeClass.name).toBe("ComfyNode");
expect(nodeClass.comfyClass).toBe(nodeNames[i]);
expect(nodeDef.name).toBe(nodeNames[i]);
expect(nodeDef).toHaveProperty("input");
expect(nodeDef).toHaveProperty("output");
expect(nodeClass.name).toBe('ComfyNode')
expect(nodeClass.comfyClass).toBe(nodeNames[i])
expect(nodeDef.name).toBe(nodeNames[i])
expect(nodeDef).toHaveProperty('input')
expect(nodeDef).toHaveProperty('output')
}
// Register custom nodes is called once after registerNode defs to allow adding other frontend nodes
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1)
// Before configure graph will be called here as the default graph is being loaded
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(1);
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(1)
// it gets sent the graph data that is going to be loaded
const graphData = mockExtension.beforeConfigureGraph.mock.calls[0][0];
const graphData = mockExtension.beforeConfigureGraph.mock.calls[0][0]
// A node created is fired for each node constructor that is called
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(
graphData.nodes.length
);
)
for (let i = 0; i < graphData.nodes.length; i++) {
expect(mockExtension.nodeCreated.mock.calls[i][0].type).toBe(
graphData.nodes[i].type
);
)
}
// Each node then calls loadedGraphNode to allow them to be updated
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(
graphData.nodes.length
);
)
for (let i = 0; i < graphData.nodes.length; i++) {
expect(mockExtension.loadedGraphNode.mock.calls[i][0].type).toBe(
graphData.nodes[i].type
);
)
}
// After configure is then called once all the setup is done
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(1);
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(1)
expect(mockExtension.setup).toHaveBeenCalledTimes(1);
expect(mockExtension.setup).toHaveBeenCalledWith(app);
expect(mockExtension.setup).toHaveBeenCalledTimes(1)
expect(mockExtension.setup).toHaveBeenCalledWith(app)
// Ensure hooks are called in the correct order
const callOrder = [
"init",
"addCustomNodeDefs",
"getCustomWidgets",
"beforeRegisterNodeDef",
"registerCustomNodes",
"beforeConfigureGraph",
"nodeCreated",
"loadedGraphNode",
"afterConfigureGraph",
"setup",
];
'init',
'addCustomNodeDefs',
'getCustomWidgets',
'beforeRegisterNodeDef',
'registerCustomNodes',
'beforeConfigureGraph',
'nodeCreated',
'loadedGraphNode',
'afterConfigureGraph',
'setup'
]
for (let i = 1; i < callOrder.length; i++) {
const fn1 = mockExtension[callOrder[i - 1]];
const fn2 = mockExtension[callOrder[i]];
const fn1 = mockExtension[callOrder[i - 1]]
const fn2 = mockExtension[callOrder[i]]
expect(fn1.mock.invocationCallOrder[0]).toBeLessThan(
fn2.mock.invocationCallOrder[0]
);
)
}
graph.clear();
graph.clear()
// Ensure adding a new node calls the correct callback
ez.LoadImage();
ez.LoadImage()
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(
graphData.nodes.length
);
)
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(
graphData.nodes.length + 1
);
expect(mockExtension.nodeCreated.mock.lastCall[0].type).toBe("LoadImage");
)
expect(mockExtension.nodeCreated.mock.lastCall[0].type).toBe('LoadImage')
// Reload the graph to ensure correct hooks are fired
await graph.reload();
await graph.reload()
// These hooks should not be fired again
expect(mockExtension.init).toHaveBeenCalledTimes(1);
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(
nodeCount
);
expect(mockExtension.setup).toHaveBeenCalledTimes(1);
expect(mockExtension.init).toHaveBeenCalledTimes(1)
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1)
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1)
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1)
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount)
expect(mockExtension.setup).toHaveBeenCalledTimes(1)
// These should be called again
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(2);
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(2)
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(
graphData.nodes.length + 2
);
)
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(
graphData.nodes.length + 1
);
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(2);
}, 15000);
)
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(2)
}, 15000)
it("allows custom nodeDefs and widgets to be registered", async () => {
it('allows custom nodeDefs and widgets to be registered', async () => {
const widgetMock = jest.fn((node, inputName, inputData, app) => {
expect(node.constructor.comfyClass).toBe("TestNode");
expect(inputName).toBe("test_input");
expect(inputData[0]).toBe("CUSTOMWIDGET");
expect(inputData[1]?.hello).toBe("world");
expect(app).toStrictEqual(app);
expect(node.constructor.comfyClass).toBe('TestNode')
expect(inputName).toBe('test_input')
expect(inputData[0]).toBe('CUSTOMWIDGET')
expect(inputData[1]?.hello).toBe('world')
expect(app).toStrictEqual(app)
return {
widget: node.addWidget("button", inputName, "hello", () => {}),
};
});
widget: node.addWidget('button', inputName, 'hello', () => {})
}
})
// Register our extension that adds a custom node + widget type
const mockExtension = {
name: "TestExtension",
name: 'TestExtension',
addCustomNodeDefs: (nodeDefs) => {
nodeDefs["TestNode"] = {
nodeDefs['TestNode'] = {
output: [],
output_name: [],
output_is_list: [],
name: "TestNode",
display_name: "TestNode",
category: "Test",
name: 'TestNode',
display_name: 'TestNode',
category: 'Test',
input: {
required: {
test_input: ["CUSTOMWIDGET", { hello: "world" }],
},
},
};
test_input: ['CUSTOMWIDGET', { hello: 'world' }]
}
}
}
},
getCustomWidgets: jest.fn(() => {
return {
CUSTOMWIDGET: widgetMock,
};
}),
};
CUSTOMWIDGET: widgetMock
}
})
}
const { graph, ez } = await start({
async preSetup(app) {
app.registerExtension(mockExtension);
},
});
app.registerExtension(mockExtension)
}
})
expect(mockExtension.getCustomWidgets).toBeCalledTimes(1);
expect(mockExtension.getCustomWidgets).toBeCalledTimes(1)
graph.clear();
expect(widgetMock).toBeCalledTimes(0);
const node = ez.TestNode();
expect(widgetMock).toBeCalledTimes(1);
graph.clear()
expect(widgetMock).toBeCalledTimes(0)
const node = ez.TestNode()
expect(widgetMock).toBeCalledTimes(1)
// Ensure our custom widget is created
expect(node.inputs.length).toBe(0);
expect(node.widgets.length).toBe(1);
const w = node.widgets[0].widget;
expect(w.name).toBe("test_input");
expect(w.type).toBe("button");
});
});
expect(node.inputs.length).toBe(0)
expect(node.widgets.length).toBe(1)
const w = node.widgets[0].widget
expect(w.name).toBe('test_input')
expect(w.type).toBe('button')
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,40 @@
import { LiteGraph } from "@comfyorg/litegraph";
import { LGraph } from "@comfyorg/litegraph";
import { LGraphNode } from "@comfyorg/litegraph";
import { LiteGraph } from '@comfyorg/litegraph'
import { LGraph } from '@comfyorg/litegraph'
import { LGraphNode } from '@comfyorg/litegraph'
function swapNodes(nodes: LGraphNode[]) {
const firstNode = nodes[0];
const lastNode = nodes[nodes.length - 1];
nodes[0] = lastNode;
nodes[nodes.length - 1] = firstNode;
return nodes;
const firstNode = nodes[0]
const lastNode = nodes[nodes.length - 1]
nodes[0] = lastNode
nodes[nodes.length - 1] = firstNode
return nodes
}
function createGraph(...nodes: LGraphNode[]) {
const graph = new LGraph();
nodes.forEach((node) => graph.add(node));
return graph;
const graph = new LGraph()
nodes.forEach((node) => graph.add(node))
return graph
}
class DummyNode extends LGraphNode {
constructor() {
super();
super()
}
}
describe("LGraph", () => {
it("should serialize deterministic node order", async () => {
LiteGraph.registerNodeType("dummy", DummyNode);
const node1 = new DummyNode();
const node2 = new DummyNode();
const graph = createGraph(node1, node2);
describe('LGraph', () => {
it('should serialize deterministic node order', async () => {
LiteGraph.registerNodeType('dummy', DummyNode)
const node1 = new DummyNode()
const node2 = new DummyNode()
const graph = createGraph(node1, node2)
const result1 = graph.serialize();
expect(result1.nodes).not.toHaveLength(0);
const result1 = graph.serialize()
expect(result1.nodes).not.toHaveLength(0)
// @ts-ignore
graph._nodes = swapNodes(graph._nodes);
const result2 = graph.serialize();
graph._nodes = swapNodes(graph._nodes)
const result2 = graph.serialize()
expect(result1).toEqual(result2);
});
});
expect(result1).toEqual(result2)
})
})

View File

@@ -1,62 +1,62 @@
import { NodeSearchService } from "@/services/nodeSearchService";
import { ComfyNodeDef } from "@/types/apiTypes";
import { NodeSearchService } from '@/services/nodeSearchService'
import { ComfyNodeDef } from '@/types/apiTypes'
const EXAMPLE_NODE_DEFS: ComfyNodeDef[] = [
{
input: {
required: {
ckpt_name: [["model1.safetensors", "model2.ckpt"]],
},
ckpt_name: [['model1.safetensors', 'model2.ckpt']]
}
},
output: ["MODEL", "CLIP", "VAE"],
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_name: ['MODEL', 'CLIP', 'VAE'],
name: 'CheckpointLoaderSimple',
display_name: 'Load Checkpoint',
description: '',
python_module: 'nodes',
category: 'loaders',
output_node: false
},
{
input: {
required: {
samples: ["LATENT"],
samples: ['LATENT'],
batch_index: [
"INT",
'INT',
{
default: 0,
min: 0,
max: 63,
},
max: 63
}
],
length: [
"INT",
'INT',
{
default: 1,
min: 1,
max: 64,
},
],
},
max: 64
}
]
}
},
output: ["LATENT"],
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_name: ['LATENT'],
name: 'LatentFromBatch',
display_name: 'Latent From Batch',
description: '',
python_module: 'nodes',
category: 'latent/batch',
output_node: false
}
]
describe("nodeSearchService", () => {
it("searches with input filter", () => {
const service = new NodeSearchService(EXAMPLE_NODE_DEFS);
const inputFilter = service.getFilterById("input");
expect(service.searchNode("L", [[inputFilter, "LATENT"]])).toHaveLength(1);
expect(service.searchNode("L")).toHaveLength(2);
});
});
describe('nodeSearchService', () => {
it('searches with input filter', () => {
const service = new NodeSearchService(EXAMPLE_NODE_DEFS)
const inputFilter = service.getFilterById('input')
expect(service.searchNode('L', [[inputFilter, 'LATENT']])).toHaveLength(1)
expect(service.searchNode('L')).toHaveLength(2)
})
})

View File

@@ -1,309 +1,307 @@
import { start } from "../utils";
import lg from "../utils/litegraph";
import { start } from '../utils'
import lg from '../utils/litegraph'
describe("users", () => {
describe('users', () => {
beforeEach(() => {
lg.setup(global);
});
lg.setup(global)
})
afterEach(() => {
lg.teardown(global);
});
lg.teardown(global)
})
function expectNoUserScreen() {
// Ensure login isnt visible
const selection = document.querySelectorAll("#comfy-user-selection")?.[0];
expect(selection["style"].display).toBe("none");
const menu = document.querySelectorAll(".comfy-menu")?.[0];
expect(window.getComputedStyle(menu)?.display).not.toBe("none");
const selection = document.querySelectorAll('#comfy-user-selection')?.[0]
expect(selection['style'].display).toBe('none')
const menu = document.querySelectorAll('.comfy-menu')?.[0]
expect(window.getComputedStyle(menu)?.display).not.toBe('none')
}
describe("multi-user", () => {
describe('multi-user', () => {
async function mockAddStylesheet() {
const utils = await import("../../src/scripts/utils");
utils.addStylesheet = jest.fn().mockReturnValue(Promise.resolve());
const utils = await import('../../src/scripts/utils')
utils.addStylesheet = jest.fn().mockReturnValue(Promise.resolve())
}
async function waitForUserScreenShow() {
// Wait for "show" to be called
const { UserSelectionScreen } = await import(
"../../src/scripts/ui/userSelection"
);
let resolve, reject;
const fn = UserSelectionScreen.prototype.show;
'../../src/scripts/ui/userSelection'
)
let resolve, reject
const fn = UserSelectionScreen.prototype.show
const p = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
resolve = res
reject = rej
})
jest
.spyOn(UserSelectionScreen.prototype, "show")
.spyOn(UserSelectionScreen.prototype, 'show')
.mockImplementation(async (...args) => {
const res = fn(...args);
await new Promise(process.nextTick); // wait for promises to resolve
resolve();
return res;
});
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."),
() => reject('timeout waiting for UserSelectionScreen to be shown.'),
500
);
await p;
await new Promise(process.nextTick); // wait for promises to resolve
)
await p
await new Promise(process.nextTick) // wait for promises to resolve
}
async function testUserScreen(onShown, users?) {
if (!users) {
users = {};
users = {}
}
const starting = start({
resetEnv: true,
userConfig: { storage: "server", users },
preSetup: mockAddStylesheet,
});
userConfig: { storage: 'server', users },
preSetup: mockAddStylesheet
})
// Ensure no current user
expect(localStorage["Comfy.userId"]).toBeFalsy();
expect(localStorage["Comfy.userName"]).toBeFalsy();
expect(localStorage['Comfy.userId']).toBeFalsy()
expect(localStorage['Comfy.userName']).toBeFalsy()
await waitForUserScreenShow();
await waitForUserScreenShow()
const selection = document.querySelectorAll("#comfy-user-selection")?.[0];
expect(selection).toBeTruthy();
const selection = document.querySelectorAll('#comfy-user-selection')?.[0]
expect(selection).toBeTruthy()
// Ensure login is visible
expect(window.getComputedStyle(selection)?.display).not.toBe("none");
expect(window.getComputedStyle(selection)?.display).not.toBe('none')
// Ensure menu is hidden
const menu = document.querySelectorAll(".comfy-menu")?.[0];
expect(window.getComputedStyle(menu)?.display).toBe("none");
const menu = document.querySelectorAll('.comfy-menu')?.[0]
expect(window.getComputedStyle(menu)?.display).toBe('none')
const isCreate = await onShown(selection);
const isCreate = await onShown(selection)
// Submit form
selection.querySelectorAll("form")[0].submit();
await new Promise(process.nextTick); // wait for promises to resolve
selection.querySelectorAll('form')[0].submit()
await new Promise(process.nextTick) // wait for promises to resolve
// Wait for start
const s = await starting;
const s = await starting
// Ensure login is removed
expect(document.querySelectorAll("#comfy-user-selection")).toHaveLength(
0
);
expect(window.getComputedStyle(menu)?.display).not.toBe("none");
expect(document.querySelectorAll('#comfy-user-selection')).toHaveLength(0)
expect(window.getComputedStyle(menu)?.display).not.toBe('none')
// Ensure settings + templates are saved
const { api } = await import("../../src/scripts/api");
expect(api.createUser).toHaveBeenCalledTimes(+isCreate);
expect(api.storeSettings).toHaveBeenCalledTimes(+isCreate);
expect(api.storeUserData).toHaveBeenCalledTimes(+isCreate);
const { api } = await import('../../src/scripts/api')
expect(api.createUser).toHaveBeenCalledTimes(+isCreate)
expect(api.storeSettings).toHaveBeenCalledTimes(+isCreate)
expect(api.storeUserData).toHaveBeenCalledTimes(+isCreate)
if (isCreate) {
expect(api.storeUserData).toHaveBeenCalledWith(
"comfy.templates.json",
'comfy.templates.json',
null,
{ stringify: false }
);
expect(s.app.isNewUserSession).toBeTruthy();
)
expect(s.app.isNewUserSession).toBeTruthy()
} else {
expect(s.app.isNewUserSession).toBeFalsy();
expect(s.app.isNewUserSession).toBeFalsy()
}
return { users, selection, ...s };
return { users, selection, ...s }
}
it("allows user creation if no users", async () => {
it('allows user creation if no users', async () => {
const { users } = await testUserScreen((selection) => {
// Ensure we have no users flag added
expect(selection.classList.contains("no-users")).toBeTruthy();
expect(selection.classList.contains('no-users')).toBeTruthy()
// Enter a username
const input = selection.getElementsByTagName("input")[0];
input.focus();
input.value = "Test User";
const input = selection.getElementsByTagName('input')[0]
input.focus()
input.value = 'Test User'
return true;
});
return true
})
expect(users).toStrictEqual({
"Test User!": "Test User",
});
'Test User!': 'Test User'
})
expect(localStorage["Comfy.userId"]).toBe("Test User!");
expect(localStorage["Comfy.userName"]).toBe("Test User");
});
it("allows user creation if no current user but other users", async () => {
expect(localStorage['Comfy.userId']).toBe('Test User!')
expect(localStorage['Comfy.userName']).toBe('Test User')
})
it('allows user creation if no current user but other users', async () => {
const users = {
"Test User 2!": "Test User 2",
};
'Test User 2!': 'Test User 2'
}
await testUserScreen((selection) => {
expect(selection.classList.contains("no-users")).toBeFalsy();
expect(selection.classList.contains('no-users')).toBeFalsy()
// Enter a username
const input = selection.getElementsByTagName("input")[0];
input.focus();
input.value = "Test User 3";
return true;
}, users);
const input = selection.getElementsByTagName('input')[0]
input.focus()
input.value = 'Test User 3'
return true
}, users)
expect(users).toStrictEqual({
"Test User 2!": "Test User 2",
"Test User 3!": "Test User 3",
});
'Test User 2!': 'Test User 2',
'Test User 3!': 'Test User 3'
})
expect(localStorage["Comfy.userId"]).toBe("Test User 3!");
expect(localStorage["Comfy.userName"]).toBe("Test User 3");
});
it("allows user selection if no current user but other users", async () => {
expect(localStorage['Comfy.userId']).toBe('Test User 3!')
expect(localStorage['Comfy.userName']).toBe('Test User 3')
})
it('allows user selection if no current user but other users', async () => {
const users = {
"A!": "A",
"B!": "B",
"C!": "C",
};
'A!': 'A',
'B!': 'B',
'C!': 'C'
}
await testUserScreen((selection) => {
expect(selection.classList.contains("no-users")).toBeFalsy();
expect(selection.classList.contains('no-users')).toBeFalsy()
// Check user list
const select = selection.getElementsByTagName("select")[0];
const options = select.getElementsByTagName("option");
const select = selection.getElementsByTagName('select')[0]
const options = select.getElementsByTagName('option')
expect(
[...options]
.filter((o) => !o.disabled)
.reduce((p, n) => {
p[n.getAttribute("value")] = n.textContent;
return p;
p[n.getAttribute('value')] = n.textContent
return p
}, {})
).toStrictEqual(users);
).toStrictEqual(users)
// Select an option
select.focus();
select.value = options[2].value;
select.focus()
select.value = options[2].value
return false;
}, users);
return false
}, users)
expect(users).toStrictEqual(users);
expect(users).toStrictEqual(users)
expect(localStorage["Comfy.userId"]).toBe("B!");
expect(localStorage["Comfy.userName"]).toBe("B");
});
it("doesnt show user screen if current user", async () => {
expect(localStorage['Comfy.userId']).toBe('B!')
expect(localStorage['Comfy.userName']).toBe('B')
})
it('doesnt show user screen if current user', async () => {
const starting = start({
resetEnv: true,
userConfig: {
storage: "server",
storage: 'server',
users: {
"User!": "User",
},
'User!': 'User'
}
},
localStorage: {
"Comfy.userId": "User!",
"Comfy.userName": "User",
},
});
await new Promise(process.nextTick); // wait for promises to resolve
'Comfy.userId': 'User!',
'Comfy.userName': 'User'
}
})
await new Promise(process.nextTick) // wait for promises to resolve
expectNoUserScreen();
expectNoUserScreen()
await starting;
});
it("allows user switching", async () => {
await starting
})
it('allows user switching', async () => {
const { app } = await start({
resetEnv: true,
userConfig: {
storage: "server",
storage: 'server',
users: {
"User!": "User",
},
'User!': 'User'
}
},
localStorage: {
"Comfy.userId": "User!",
"Comfy.userName": "User",
},
});
'Comfy.userId': 'User!',
'Comfy.userName': 'User'
}
})
// cant actually test switching user easily but can check the setting is present
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeTruthy();
});
});
describe("single-user", () => {
it("doesnt show user creation if no default user", async () => {
expect(app.ui.settings.settingsLookup['Comfy.SwitchUser']).toBeTruthy()
})
})
describe('single-user', () => {
it('doesnt show user creation if no default user', async () => {
const { app } = await start({
resetEnv: true,
userConfig: { migrated: false, storage: "server" },
});
expectNoUserScreen();
userConfig: { migrated: false, storage: 'server' }
})
expectNoUserScreen()
// It should store the settings
const { api } = await import("../../src/scripts/api");
expect(api.storeSettings).toHaveBeenCalledTimes(1);
expect(api.storeUserData).toHaveBeenCalledTimes(1);
const { api } = await import('../../src/scripts/api')
expect(api.storeSettings).toHaveBeenCalledTimes(1)
expect(api.storeUserData).toHaveBeenCalledTimes(1)
expect(api.storeUserData).toHaveBeenCalledWith(
"comfy.templates.json",
'comfy.templates.json',
null,
{ stringify: false }
);
expect(app.isNewUserSession).toBeTruthy();
});
it("doesnt show user creation if default user", async () => {
)
expect(app.isNewUserSession).toBeTruthy()
})
it('doesnt show user creation if default user', async () => {
const { app } = await start({
resetEnv: true,
userConfig: { migrated: true, storage: "server" },
});
expectNoUserScreen();
userConfig: { migrated: true, storage: 'server' }
})
expectNoUserScreen()
// It should store the settings
const { api } = await import("../../src/scripts/api");
expect(api.storeSettings).toHaveBeenCalledTimes(0);
expect(api.storeUserData).toHaveBeenCalledTimes(0);
expect(app.isNewUserSession).toBeFalsy();
});
it("doesnt allow user switching", async () => {
const { api } = await import('../../src/scripts/api')
expect(api.storeSettings).toHaveBeenCalledTimes(0)
expect(api.storeUserData).toHaveBeenCalledTimes(0)
expect(app.isNewUserSession).toBeFalsy()
})
it('doesnt allow user switching', async () => {
const { app } = await start({
resetEnv: true,
userConfig: { migrated: true, storage: "server" },
});
expectNoUserScreen();
userConfig: { migrated: true, storage: 'server' }
})
expectNoUserScreen()
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy();
});
});
describe("browser-user", () => {
it("doesnt show user creation if no default user", async () => {
expect(app.ui.settings.settingsLookup['Comfy.SwitchUser']).toBeFalsy()
})
})
describe('browser-user', () => {
it('doesnt show user creation if no default user', async () => {
const { app } = await start({
resetEnv: true,
userConfig: { migrated: false, storage: "browser" },
});
expectNoUserScreen();
userConfig: { migrated: false, storage: 'browser' }
})
expectNoUserScreen()
// It should store the settings
const { api } = await import("../../src/scripts/api");
expect(api.storeSettings).toHaveBeenCalledTimes(0);
expect(api.storeUserData).toHaveBeenCalledTimes(0);
expect(app.isNewUserSession).toBeFalsy();
});
it("doesnt show user creation if default user", async () => {
const { api } = await import('../../src/scripts/api')
expect(api.storeSettings).toHaveBeenCalledTimes(0)
expect(api.storeUserData).toHaveBeenCalledTimes(0)
expect(app.isNewUserSession).toBeFalsy()
})
it('doesnt show user creation if default user', async () => {
const { app } = await start({
resetEnv: true,
userConfig: { migrated: true, storage: "server" },
});
expectNoUserScreen();
userConfig: { migrated: true, storage: 'server' }
})
expectNoUserScreen()
// It should store the settings
const { api } = await import("../../src/scripts/api");
expect(api.storeSettings).toHaveBeenCalledTimes(0);
expect(api.storeUserData).toHaveBeenCalledTimes(0);
expect(app.isNewUserSession).toBeFalsy();
});
it("doesnt allow user switching", async () => {
const { api } = await import('../../src/scripts/api')
expect(api.storeSettings).toHaveBeenCalledTimes(0)
expect(api.storeUserData).toHaveBeenCalledTimes(0)
expect(app.isNewUserSession).toBeFalsy()
})
it('doesnt allow user switching', async () => {
const { app } = await start({
resetEnv: true,
userConfig: { migrated: true, storage: "browser" },
});
expectNoUserScreen();
userConfig: { migrated: true, storage: 'browser' }
})
expectNoUserScreen()
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy();
});
});
});
expect(app.ui.settings.settingsLookup['Comfy.SwitchUser']).toBeFalsy()
})
})
})

View File

@@ -3,9 +3,9 @@ import {
makeNodeDef,
checkBeforeAndAfterReload,
assertNotNullOrUndefined,
createDefaultWorkflow,
} from "../utils";
import lg from "../utils/litegraph";
createDefaultWorkflow
} from '../utils'
import lg from '../utils/litegraph'
/**
* @typedef { import("../utils/ezgraph") } Ez
@@ -28,71 +28,70 @@ async function connectPrimitiveAndReload(
controlWidgetCount = 0
) {
// Connect to primitive and ensure its still connected after
let primitive = ez.PrimitiveNode();
primitive.outputs[0].connectTo(input);
let primitive = ez.PrimitiveNode()
primitive.outputs[0].connectTo(input)
await checkBeforeAndAfterReload(graph, async () => {
primitive = graph.find(primitive);
let { connections } = primitive.outputs[0];
expect(connections).toHaveLength(1);
expect(connections[0].targetNode.id).toBe(input.node.node.id);
primitive = graph.find(primitive)
let { connections } = primitive.outputs[0]
expect(connections).toHaveLength(1)
expect(connections[0].targetNode.id).toBe(input.node.node.id)
// Ensure widget is correct type
const valueWidget = primitive.widgets.value;
expect(valueWidget.widget.type).toBe(widgetType);
const valueWidget = primitive.widgets.value
expect(valueWidget.widget.type).toBe(widgetType)
// Check if control_after_generate should be added
if (controlWidgetCount) {
const controlWidget = primitive.widgets.control_after_generate;
expect(controlWidget.widget.type).toBe("combo");
if (widgetType === "combo") {
const filterWidget = primitive.widgets.control_filter_list;
expect(filterWidget.widget.type).toBe("string");
const controlWidget = primitive.widgets.control_after_generate
expect(controlWidget.widget.type).toBe('combo')
if (widgetType === 'combo') {
const filterWidget = primitive.widgets.control_filter_list
expect(filterWidget.widget.type).toBe('string')
}
}
// Ensure we dont have other widgets
expect(primitive.node.widgets).toHaveLength(1 + controlWidgetCount);
});
expect(primitive.node.widgets).toHaveLength(1 + controlWidgetCount)
})
return primitive;
return primitive
}
describe("widget inputs", () => {
describe('widget inputs', () => {
beforeEach(() => {
lg.setup(global);
});
lg.setup(global)
})
afterEach(() => {
lg.teardown(global);
});
[
{ name: "int", type: "INT", widget: "number", control: 1 },
{ name: "float", type: "FLOAT", widget: "number", control: 1 },
{ name: "text", type: "STRING" },
lg.teardown(global)
})
;[
{ name: 'int', type: 'INT', widget: 'number', control: 1 },
{ name: 'float', type: 'FLOAT', widget: 'number', control: 1 },
{ name: 'text', type: 'STRING' },
{
name: "customtext",
type: "STRING",
opt: { multiline: true },
name: 'customtext',
type: 'STRING',
opt: { multiline: true }
},
{ name: "toggle", type: "BOOLEAN" },
{ name: "combo", type: ["a", "b", "c"], control: 2 },
{ name: 'toggle', type: 'BOOLEAN' },
{ name: 'combo', type: ['a', 'b', 'c'], control: 2 }
].forEach((c) => {
test(`widget conversion + primitive works on ${c.name}`, async () => {
const { ez, graph } = await start({
mockNodeDefs: makeNodeDef("TestNode", {
[c.name]: [c.type, c.opt ?? {}],
}),
});
mockNodeDefs: makeNodeDef('TestNode', {
[c.name]: [c.type, c.opt ?? {}]
})
})
// Create test node and convert to input
const n = ez.TestNode();
const w = n.widgets[c.name];
w.convertToInput();
expect(w.isConvertedToInput).toBeTruthy();
const input = w.getConvertedInput();
expect(input).toBeTruthy();
const n = ez.TestNode()
const w = n.widgets[c.name]
w.convertToInput()
expect(w.isConvertedToInput).toBeTruthy()
const input = w.getConvertedInput()
expect(input).toBeTruthy()
// @ts-ignore : input is valid here
await connectPrimitiveAndReload(
@@ -101,91 +100,91 @@ describe("widget inputs", () => {
input,
c.widget ?? c.name,
c.control
);
});
});
)
})
})
test("converted widget works after reload", async () => {
const { ez, graph } = await start();
let n = ez.CheckpointLoaderSimple();
test('converted widget works after reload', async () => {
const { ez, graph } = await start()
let n = ez.CheckpointLoaderSimple()
const inputCount = n.inputs.length;
const inputCount = n.inputs.length
// Convert ckpt name to an input
n.widgets.ckpt_name.convertToInput();
expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
expect(n.inputs.ckpt_name).toBeTruthy();
expect(n.inputs.length).toEqual(inputCount + 1);
n.widgets.ckpt_name.convertToInput()
expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy()
expect(n.inputs.ckpt_name).toBeTruthy()
expect(n.inputs.length).toEqual(inputCount + 1)
// Convert back to widget and ensure input is removed
n.widgets.ckpt_name.convertToWidget();
expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
expect(n.inputs.ckpt_name).toBeFalsy();
expect(n.inputs.length).toEqual(inputCount);
n.widgets.ckpt_name.convertToWidget()
expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy()
expect(n.inputs.ckpt_name).toBeFalsy()
expect(n.inputs.length).toEqual(inputCount)
// Convert again and reload the graph to ensure it maintains state
n.widgets.ckpt_name.convertToInput();
expect(n.inputs.length).toEqual(inputCount + 1);
n.widgets.ckpt_name.convertToInput()
expect(n.inputs.length).toEqual(inputCount + 1)
const primitive = await connectPrimitiveAndReload(
ez,
graph,
n.inputs.ckpt_name,
"combo",
'combo',
2
);
)
// Disconnect & reconnect
primitive.outputs[0].connections[0].disconnect();
let { connections } = primitive.outputs[0];
expect(connections).toHaveLength(0);
primitive.outputs[0].connections[0].disconnect()
let { connections } = primitive.outputs[0]
expect(connections).toHaveLength(0)
primitive.outputs[0].connectTo(n.inputs.ckpt_name);
({ connections } = primitive.outputs[0]);
expect(connections).toHaveLength(1);
expect(connections[0].targetNode.id).toBe(n.node.id);
primitive.outputs[0].connectTo(n.inputs.ckpt_name)
;({ connections } = primitive.outputs[0])
expect(connections).toHaveLength(1)
expect(connections[0].targetNode.id).toBe(n.node.id)
// Convert back to widget and ensure input is removed
n.widgets.ckpt_name.convertToWidget();
expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
expect(n.inputs.ckpt_name).toBeFalsy();
expect(n.inputs.length).toEqual(inputCount);
});
n.widgets.ckpt_name.convertToWidget()
expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy()
expect(n.inputs.ckpt_name).toBeFalsy()
expect(n.inputs.length).toEqual(inputCount)
})
test("converted widget works on clone", async () => {
const { graph, ez } = await start();
let n = ez.CheckpointLoaderSimple();
test('converted widget works on clone', async () => {
const { graph, ez } = await start()
let n = ez.CheckpointLoaderSimple()
// Convert the widget to an input
n.widgets.ckpt_name.convertToInput();
expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
n.widgets.ckpt_name.convertToInput()
expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy()
// Clone the node
n.menu["Clone"].call();
expect(graph.nodes).toHaveLength(2);
const clone = graph.nodes[1];
expect(clone.id).not.toEqual(n.id);
n.menu['Clone'].call()
expect(graph.nodes).toHaveLength(2)
const clone = graph.nodes[1]
expect(clone.id).not.toEqual(n.id)
// Ensure the clone has an input
expect(clone.widgets.ckpt_name.isConvertedToInput).toBeTruthy();
expect(clone.inputs.ckpt_name).toBeTruthy();
expect(clone.widgets.ckpt_name.isConvertedToInput).toBeTruthy()
expect(clone.inputs.ckpt_name).toBeTruthy()
// Ensure primitive connects to both nodes
let primitive = ez.PrimitiveNode();
primitive.outputs[0].connectTo(n.inputs.ckpt_name);
primitive.outputs[0].connectTo(clone.inputs.ckpt_name);
expect(primitive.outputs[0].connections).toHaveLength(2);
let primitive = ez.PrimitiveNode()
primitive.outputs[0].connectTo(n.inputs.ckpt_name)
primitive.outputs[0].connectTo(clone.inputs.ckpt_name)
expect(primitive.outputs[0].connections).toHaveLength(2)
// Convert back to widget and ensure input is removed
clone.widgets.ckpt_name.convertToWidget();
expect(clone.widgets.ckpt_name.isConvertedToInput).toBeFalsy();
expect(clone.inputs.ckpt_name).toBeFalsy();
});
clone.widgets.ckpt_name.convertToWidget()
expect(clone.widgets.ckpt_name.isConvertedToInput).toBeFalsy()
expect(clone.inputs.ckpt_name).toBeFalsy()
})
test("shows missing node error on custom node with converted input", async () => {
const { graph } = await start();
test('shows missing node error on custom node with converted input', async () => {
const { graph } = await start()
const dialogShow = jest.spyOn(graph.app.ui.dialog, "show");
const dialogShow = jest.spyOn(graph.app.ui.dialog, 'show')
await graph.app.loadGraphData({
last_node_id: 3,
@@ -193,7 +192,7 @@ describe("widget inputs", () => {
nodes: [
{
id: 1,
type: "TestNode",
type: 'TestNode',
pos: [41.87329101561909, 389.7381480823742],
size: { 0: 220, 1: 374 },
flags: {},
@@ -201,426 +200,426 @@ describe("widget inputs", () => {
mode: 0,
inputs: [
{
name: "test",
type: "FLOAT",
name: 'test',
type: 'FLOAT',
link: 4,
widget: { name: "test" },
slot_index: 0,
},
widget: { name: 'test' },
slot_index: 0
}
],
outputs: [],
properties: { "Node name for S&R": "TestNode" },
widgets_values: [1],
properties: { 'Node name for S&R': 'TestNode' },
widgets_values: [1]
},
{
id: 3,
type: "PrimitiveNode",
type: 'PrimitiveNode',
pos: [-312, 433],
size: { 0: 210, 1: 82 },
flags: {},
order: 0,
mode: 0,
outputs: [{ links: [4], widget: { name: "test" } }],
title: "test",
properties: {},
},
outputs: [{ links: [4], widget: { name: 'test' } }],
title: 'test',
properties: {}
}
],
links: [[4, 3, 0, 1, 6, "FLOAT"]],
links: [[4, 3, 0, 1, 6, 'FLOAT']],
groups: [],
config: {},
extra: {},
version: 0.4,
});
version: 0.4
})
expect(dialogShow).toBeCalledTimes(1);
expect(dialogShow).toBeCalledTimes(1)
// @ts-ignore
expect(dialogShow.mock.calls[0][0].innerHTML).toContain(
"the following node types were not found"
);
'the following node types were not found'
)
// @ts-ignore
expect(dialogShow.mock.calls[0][0].innerHTML).toContain("TestNode");
});
expect(dialogShow.mock.calls[0][0].innerHTML).toContain('TestNode')
})
test("defaultInput widgets can be converted back to inputs", async () => {
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
let n = ez.TestNode();
let w = n.widgets.example;
expect(w.isConvertedToInput).toBeTruthy();
let input = w.getConvertedInput();
expect(input).toBeTruthy();
let n = ez.TestNode()
let w = n.widgets.example
expect(w.isConvertedToInput).toBeTruthy()
let input = w.getConvertedInput()
expect(input).toBeTruthy()
// Ensure it can be converted to
w.convertToWidget();
expect(w.isConvertedToInput).toBeFalsy();
expect(n.inputs.length).toEqual(0);
w.convertToWidget()
expect(w.isConvertedToInput).toBeFalsy()
expect(n.inputs.length).toEqual(0)
// and from
w.convertToInput();
expect(w.isConvertedToInput).toBeTruthy();
input = w.getConvertedInput();
w.convertToInput()
expect(w.isConvertedToInput).toBeTruthy()
input = w.getConvertedInput()
// Reload and ensure it still only has 1 converted widget
if (!assertNotNullOrUndefined(input)) return;
if (!assertNotNullOrUndefined(input)) return
await connectPrimitiveAndReload(ez, graph, input, "number", 1);
n = graph.find(n);
expect(n.widgets).toHaveLength(1);
w = n.widgets.example;
expect(w.isConvertedToInput).toBeTruthy();
await connectPrimitiveAndReload(ez, graph, input, 'number', 1)
n = graph.find(n)
expect(n.widgets).toHaveLength(1)
w = n.widgets.example
expect(w.isConvertedToInput).toBeTruthy()
// Convert back to widget and ensure it is still a widget after reload
w.convertToWidget();
await graph.reload();
n = graph.find(n);
expect(n.widgets).toHaveLength(1);
expect(n.widgets[0].isConvertedToInput).toBeFalsy();
expect(n.inputs.length).toEqual(0);
});
w.convertToWidget()
await graph.reload()
n = graph.find(n)
expect(n.widgets).toHaveLength(1)
expect(n.widgets[0].isConvertedToInput).toBeFalsy()
expect(n.inputs.length).toEqual(0)
})
test("forceInput widgets can not be converted back to inputs", async () => {
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
let n = ez.TestNode();
let w = n.widgets.example;
expect(w.isConvertedToInput).toBeTruthy();
const input = w.getConvertedInput();
expect(input).toBeTruthy();
let n = ez.TestNode()
let w = n.widgets.example
expect(w.isConvertedToInput).toBeTruthy()
const input = w.getConvertedInput()
expect(input).toBeTruthy()
// Convert to widget should error
expect(() => w.convertToWidget()).toThrow();
expect(() => w.convertToWidget()).toThrow()
// Reload and ensure it still only has 1 converted widget
if (assertNotNullOrUndefined(input)) {
await connectPrimitiveAndReload(ez, graph, input, "number", 1);
n = graph.find(n);
expect(n.widgets).toHaveLength(1);
expect(n.widgets.example.isConvertedToInput).toBeTruthy();
await connectPrimitiveAndReload(ez, graph, input, 'number', 1)
n = graph.find(n)
expect(n.widgets).toHaveLength(1)
expect(n.widgets.example.isConvertedToInput).toBeTruthy()
}
});
})
test("primitive can connect to matching combos on converted widgets", async () => {
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('TestNode1', {
example: [['A', 'B', 'C'], { forceInput: true }]
}),
...makeNodeDef("TestNode2", {
example: [["A", "B", "C"], { forceInput: true }],
}),
},
});
...makeNodeDef('TestNode2', {
example: [['A', 'B', 'C'], { forceInput: true }]
})
}
})
const n1 = ez.TestNode1();
const n2 = ez.TestNode2();
const p = ez.PrimitiveNode();
p.outputs[0].connectTo(n1.inputs[0]);
p.outputs[0].connectTo(n2.inputs[0]);
expect(p.outputs[0].connections).toHaveLength(2);
const valueWidget = p.widgets.value;
expect(valueWidget.widget.type).toBe("combo");
expect(valueWidget.widget.options.values).toEqual(["A", "B", "C"]);
});
const n1 = ez.TestNode1()
const n2 = ez.TestNode2()
const p = ez.PrimitiveNode()
p.outputs[0].connectTo(n1.inputs[0])
p.outputs[0].connectTo(n2.inputs[0])
expect(p.outputs[0].connections).toHaveLength(2)
const valueWidget = p.widgets.value
expect(valueWidget.widget.type).toBe('combo')
expect(valueWidget.widget.options.values).toEqual(['A', 'B', 'C'])
})
test("primitive can not connect to non matching combos on converted widgets", async () => {
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('TestNode1', {
example: [['A', 'B', 'C'], { forceInput: true }]
}),
...makeNodeDef("TestNode2", {
example: [["A", "B"], { forceInput: true }],
}),
},
});
...makeNodeDef('TestNode2', {
example: [['A', 'B'], { forceInput: true }]
})
}
})
const n1 = ez.TestNode1();
const n2 = ez.TestNode2();
const p = ez.PrimitiveNode();
p.outputs[0].connectTo(n1.inputs[0]);
expect(() => p.outputs[0].connectTo(n2.inputs[0])).toThrow();
expect(p.outputs[0].connections).toHaveLength(1);
});
const n1 = ez.TestNode1()
const n2 = ez.TestNode2()
const p = ez.PrimitiveNode()
p.outputs[0].connectTo(n1.inputs[0])
expect(() => p.outputs[0].connectTo(n2.inputs[0])).toThrow()
expect(p.outputs[0].connections).toHaveLength(1)
})
test("combo output can not connect to non matching combos list input", async () => {
test('combo output can not connect to non matching combos list input', async () => {
const { ez } = await start({
mockNodeDefs: {
...makeNodeDef("TestNode1", {}, [["A", "B"]]),
...makeNodeDef("TestNode2", {
example: [["A", "B"], { forceInput: true }],
...makeNodeDef('TestNode1', {}, [['A', 'B']]),
...makeNodeDef('TestNode2', {
example: [['A', 'B'], { forceInput: true }]
}),
...makeNodeDef("TestNode3", {
example: [["A", "B", "C"], { forceInput: true }],
}),
},
});
...makeNodeDef('TestNode3', {
example: [['A', 'B', 'C'], { forceInput: true }]
})
}
})
const n1 = ez.TestNode1();
const n2 = ez.TestNode2();
const n3 = ez.TestNode3();
const n1 = ez.TestNode1()
const n2 = ez.TestNode2()
const n3 = ez.TestNode3()
n1.outputs[0].connectTo(n2.inputs[0]);
expect(() => n1.outputs[0].connectTo(n3.inputs[0])).toThrow();
});
n1.outputs[0].connectTo(n2.inputs[0])
expect(() => n1.outputs[0].connectTo(n3.inputs[0])).toThrow()
})
test("combo primitive can filter list when control_after_generate called", async () => {
test('combo primitive can filter list when control_after_generate called', async () => {
const { ez } = await start({
mockNodeDefs: {
...makeNodeDef("TestNode1", {
...makeNodeDef('TestNode1', {
example: [
["A", "B", "C", "D", "AA", "BB", "CC", "DD", "AAA", "BBB"],
{},
],
}),
},
});
['A', 'B', 'C', 'D', 'AA', 'BB', 'CC', 'DD', 'AAA', 'BBB'],
{}
]
})
}
})
const n1 = ez.TestNode1();
n1.widgets.example.convertToInput();
const p = ez.PrimitiveNode();
p.outputs[0].connectTo(n1.inputs[0]);
const n1 = ez.TestNode1()
n1.widgets.example.convertToInput()
const p = ez.PrimitiveNode()
p.outputs[0].connectTo(n1.inputs[0])
const value = p.widgets.value;
const control = p.widgets.control_after_generate.widget;
const filter = p.widgets.control_filter_list;
const value = p.widgets.value
const control = p.widgets.control_after_generate.widget
const filter = p.widgets.control_filter_list
expect(p.widgets.length).toBe(3);
control.value = "increment";
expect(value.value).toBe("A");
expect(p.widgets.length).toBe(3)
control.value = 'increment'
expect(value.value).toBe('A')
// Manually trigger after queue when set to increment
control["afterQueued"]();
expect(value.value).toBe("B");
control['afterQueued']()
expect(value.value).toBe('B')
// Filter to items containing D
filter.value = "D";
control["afterQueued"]();
expect(value.value).toBe("D");
control["afterQueued"]();
expect(value.value).toBe("DD");
filter.value = 'D'
control['afterQueued']()
expect(value.value).toBe('D')
control['afterQueued']()
expect(value.value).toBe('DD')
// Check decrement
value.value = "BBB";
control.value = "decrement";
filter.value = "B";
control["afterQueued"]();
expect(value.value).toBe("BB");
control["afterQueued"]();
expect(value.value).toBe("B");
value.value = 'BBB'
control.value = 'decrement'
filter.value = 'B'
control['afterQueued']()
expect(value.value).toBe('BB')
control['afterQueued']()
expect(value.value).toBe('B')
// Check regex works
value.value = "BBB";
filter.value = "/[AB]|^C$/";
control["afterQueued"]();
expect(value.value).toBe("AAA");
control["afterQueued"]();
expect(value.value).toBe("BB");
control["afterQueued"]();
expect(value.value).toBe("AA");
control["afterQueued"]();
expect(value.value).toBe("C");
control["afterQueued"]();
expect(value.value).toBe("B");
control["afterQueued"]();
expect(value.value).toBe("A");
value.value = 'BBB'
filter.value = '/[AB]|^C$/'
control['afterQueued']()
expect(value.value).toBe('AAA')
control['afterQueued']()
expect(value.value).toBe('BB')
control['afterQueued']()
expect(value.value).toBe('AA')
control['afterQueued']()
expect(value.value).toBe('C')
control['afterQueued']()
expect(value.value).toBe('B')
control['afterQueued']()
expect(value.value).toBe('A')
// Check random
control.value = "randomize";
filter.value = "/D/";
control.value = 'randomize'
filter.value = '/D/'
for (let i = 0; i < 100; i++) {
control["afterQueued"]();
expect(value.value === "D" || value.value === "DD").toBeTruthy();
control['afterQueued']()
expect(value.value === 'D' || value.value === 'DD').toBeTruthy()
}
// Ensure it doesnt apply when fixed
control.value = "fixed";
value.value = "B";
filter.value = "C";
control["afterQueued"]();
expect(value.value).toBe("B");
});
control.value = 'fixed'
value.value = 'B'
filter.value = 'C'
control['afterQueued']()
expect(value.value).toBe('B')
})
describe("reroutes", () => {
describe('reroutes', () => {
async function checkOutput(graph, values) {
expect((await graph.toPrompt()).output).toStrictEqual({
1: {
inputs: { ckpt_name: "model1.safetensors" },
class_type: "CheckpointLoaderSimple",
inputs: { ckpt_name: 'model1.safetensors' },
class_type: 'CheckpointLoaderSimple'
},
2: {
inputs: { text: "positive", clip: ["1", 1] },
class_type: "CLIPTextEncode",
inputs: { text: 'positive', clip: ['1', 1] },
class_type: 'CLIPTextEncode'
},
3: {
inputs: { text: "negative", clip: ["1", 1] },
class_type: "CLIPTextEncode",
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,
batch_size: values?.batch_size ?? 1
},
class_type: "EmptyLatentImage",
class_type: 'EmptyLatentImage'
},
5: {
inputs: {
seed: 0,
steps: 20,
cfg: 8,
sampler_name: "euler",
scheduler: values?.scheduler ?? "normal",
sampler_name: 'euler',
scheduler: values?.scheduler ?? 'normal',
denoise: 1,
model: ["1", 0],
positive: ["2", 0],
negative: ["3", 0],
latent_image: ["4", 0],
model: ['1', 0],
positive: ['2', 0],
negative: ['3', 0],
latent_image: ['4', 0]
},
class_type: "KSampler",
class_type: 'KSampler'
},
6: {
inputs: { samples: ["5", 0], vae: ["1", 2] },
class_type: "VAEDecode",
inputs: { samples: ['5', 0], vae: ['1', 2] },
class_type: 'VAEDecode'
},
7: {
inputs: {
filename_prefix: values.filename_prefix ?? "ComfyUI",
images: ["6", 0],
filename_prefix: values.filename_prefix ?? 'ComfyUI',
images: ['6', 0]
},
class_type: "SaveImage",
},
});
class_type: 'SaveImage'
}
})
}
async function waitForWidget(node) {
// widgets are created slightly after the graph is ready
// hard to find an exact hook to get these so just wait for them to be ready
for (let i = 0; i < 10; i++) {
await new Promise((r) => setTimeout(r, 10));
await new Promise((r) => setTimeout(r, 10))
if (node.widgets?.value) {
return;
return
}
}
}
it("can connect primitive via a reroute path to a widget input", async () => {
const { ez, graph } = await start();
const nodes = createDefaultWorkflow(ez, graph);
it('can connect primitive via a reroute path to a widget input', async () => {
const { ez, graph } = await start()
const nodes = createDefaultWorkflow(ez, graph)
nodes.empty.widgets.width.convertToInput();
nodes.sampler.widgets.scheduler.convertToInput();
nodes.save.widgets.filename_prefix.convertToInput();
nodes.empty.widgets.width.convertToInput()
nodes.sampler.widgets.scheduler.convertToInput()
nodes.save.widgets.filename_prefix.convertToInput()
let widthReroute = ez.Reroute();
let schedulerReroute = ez.Reroute();
let fileReroute = ez.Reroute();
let widthReroute = ez.Reroute()
let schedulerReroute = ez.Reroute()
let fileReroute = ez.Reroute()
let widthNext = widthReroute;
let schedulerNext = schedulerReroute;
let fileNext = fileReroute;
let widthNext = widthReroute
let schedulerNext = schedulerReroute
let fileNext = fileReroute
for (let i = 0; i < 5; i++) {
let next = ez.Reroute();
widthNext.outputs[0].connectTo(next.inputs[0]);
widthNext = next;
let next = ez.Reroute()
widthNext.outputs[0].connectTo(next.inputs[0])
widthNext = next
next = ez.Reroute();
schedulerNext.outputs[0].connectTo(next.inputs[0]);
schedulerNext = next;
next = ez.Reroute()
schedulerNext.outputs[0].connectTo(next.inputs[0])
schedulerNext = next
next = ez.Reroute();
fileNext.outputs[0].connectTo(next.inputs[0]);
fileNext = next;
next = ez.Reroute()
fileNext.outputs[0].connectTo(next.inputs[0])
fileNext = next
}
widthNext.outputs[0].connectTo(nodes.empty.inputs.width);
schedulerNext.outputs[0].connectTo(nodes.sampler.inputs.scheduler);
fileNext.outputs[0].connectTo(nodes.save.inputs.filename_prefix);
widthNext.outputs[0].connectTo(nodes.empty.inputs.width)
schedulerNext.outputs[0].connectTo(nodes.sampler.inputs.scheduler)
fileNext.outputs[0].connectTo(nodes.save.inputs.filename_prefix)
let widthPrimitive = ez.PrimitiveNode();
let schedulerPrimitive = ez.PrimitiveNode();
let filePrimitive = ez.PrimitiveNode();
let widthPrimitive = ez.PrimitiveNode()
let schedulerPrimitive = ez.PrimitiveNode()
let filePrimitive = ez.PrimitiveNode()
widthPrimitive.outputs[0].connectTo(widthReroute.inputs[0]);
schedulerPrimitive.outputs[0].connectTo(schedulerReroute.inputs[0]);
filePrimitive.outputs[0].connectTo(fileReroute.inputs[0]);
expect(widthPrimitive.widgets.value.value).toBe(512);
widthPrimitive.widgets.value.value = 1024;
expect(schedulerPrimitive.widgets.value.value).toBe("normal");
schedulerPrimitive.widgets.value.value = "simple";
expect(filePrimitive.widgets.value.value).toBe("ComfyUI");
filePrimitive.widgets.value.value = "ComfyTest";
widthPrimitive.outputs[0].connectTo(widthReroute.inputs[0])
schedulerPrimitive.outputs[0].connectTo(schedulerReroute.inputs[0])
filePrimitive.outputs[0].connectTo(fileReroute.inputs[0])
expect(widthPrimitive.widgets.value.value).toBe(512)
widthPrimitive.widgets.value.value = 1024
expect(schedulerPrimitive.widgets.value.value).toBe('normal')
schedulerPrimitive.widgets.value.value = 'simple'
expect(filePrimitive.widgets.value.value).toBe('ComfyUI')
filePrimitive.widgets.value.value = 'ComfyTest'
await checkBeforeAndAfterReload(graph, async () => {
widthPrimitive = graph.find(widthPrimitive);
schedulerPrimitive = graph.find(schedulerPrimitive);
filePrimitive = graph.find(filePrimitive);
await waitForWidget(filePrimitive);
expect(widthPrimitive.widgets.length).toBe(2);
expect(schedulerPrimitive.widgets.length).toBe(3);
expect(filePrimitive.widgets.length).toBe(1);
widthPrimitive = graph.find(widthPrimitive)
schedulerPrimitive = graph.find(schedulerPrimitive)
filePrimitive = graph.find(filePrimitive)
await waitForWidget(filePrimitive)
expect(widthPrimitive.widgets.length).toBe(2)
expect(schedulerPrimitive.widgets.length).toBe(3)
expect(filePrimitive.widgets.length).toBe(1)
await checkOutput(graph, {
width: 1024,
scheduler: "simple",
filename_prefix: "ComfyTest",
});
});
});
it("can connect primitive via a reroute path to multiple widget inputs", async () => {
const { ez, graph } = await start();
const nodes = createDefaultWorkflow(ez, graph);
scheduler: 'simple',
filename_prefix: 'ComfyTest'
})
})
})
it('can connect primitive via a reroute path to multiple widget inputs', async () => {
const { ez, graph } = await start()
const nodes = createDefaultWorkflow(ez, graph)
nodes.empty.widgets.width.convertToInput();
nodes.empty.widgets.height.convertToInput();
nodes.empty.widgets.batch_size.convertToInput();
nodes.empty.widgets.width.convertToInput()
nodes.empty.widgets.height.convertToInput()
nodes.empty.widgets.batch_size.convertToInput()
let reroute = ez.Reroute();
let prevReroute = reroute;
let reroute = ez.Reroute()
let prevReroute = reroute
for (let i = 0; i < 5; i++) {
const next = ez.Reroute();
prevReroute.outputs[0].connectTo(next.inputs[0]);
prevReroute = next;
const next = ez.Reroute()
prevReroute.outputs[0].connectTo(next.inputs[0])
prevReroute = next
}
const r1 = ez.Reroute(prevReroute.outputs[0]);
const r2 = ez.Reroute(prevReroute.outputs[0]);
const r3 = ez.Reroute(r2.outputs[0]);
const r4 = ez.Reroute(r2.outputs[0]);
const r1 = ez.Reroute(prevReroute.outputs[0])
const r2 = ez.Reroute(prevReroute.outputs[0])
const r3 = ez.Reroute(r2.outputs[0])
const r4 = ez.Reroute(r2.outputs[0])
r1.outputs[0].connectTo(nodes.empty.inputs.width);
r3.outputs[0].connectTo(nodes.empty.inputs.height);
r4.outputs[0].connectTo(nodes.empty.inputs.batch_size);
r1.outputs[0].connectTo(nodes.empty.inputs.width)
r3.outputs[0].connectTo(nodes.empty.inputs.height)
r4.outputs[0].connectTo(nodes.empty.inputs.batch_size)
let primitive = ez.PrimitiveNode();
primitive.outputs[0].connectTo(reroute.inputs[0]);
expect(primitive.widgets.value.value).toBe(1);
primitive.widgets.value.value = 64;
let primitive = ez.PrimitiveNode()
primitive.outputs[0].connectTo(reroute.inputs[0])
expect(primitive.widgets.value.value).toBe(1)
primitive.widgets.value.value = 64
await checkBeforeAndAfterReload(graph, async (r) => {
primitive = graph.find(primitive);
await waitForWidget(primitive);
primitive = graph.find(primitive)
await waitForWidget(primitive)
// Ensure widget configs are merged
expect(primitive.widgets.value.widget.options?.min).toBe(16); // width/height min
expect(primitive.widgets.value.widget.options?.max).toBe(4096); // batch max
expect(primitive.widgets.value.widget.options?.step).toBe(80); // width/height step * 10
expect(primitive.widgets.value.widget.options?.min).toBe(16) // width/height min
expect(primitive.widgets.value.widget.options?.max).toBe(4096) // batch max
expect(primitive.widgets.value.widget.options?.step).toBe(80) // width/height step * 10
await checkOutput(graph, {
width: 64,
height: 64,
batch_size: 64,
});
});
});
});
});
batch_size: 64
})
})
})
})
})

View File

@@ -1,4 +1,4 @@
import type { LiteGraph, LGraphCanvas } from "@comfyorg/litegraph";
import type { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph'
/**
* @typedef { import("./src/scripts/app")["app"] } app
@@ -11,34 +11,28 @@ import type { LiteGraph, LGraphCanvas } from "@comfyorg/litegraph";
* @typedef { (...args: EzOutput[] | [...EzOutput[], Record<string, unknown>]) => EzNode } EzNodeFactory
*/
export type EzNameSpace = Record<string, (...args) => EzNode>;
export type EzNameSpace = Record<string, (...args) => EzNode>
export class EzConnection {
/** @type { app } */
app;
app
/** @type { InstanceType<LG["LLink"]> } */
link;
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() {
return this.originNode.outputs[this.link.origin_slot];
return this.originNode.outputs[this.link.origin_slot]
}
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() {
return this.targetNode.inputs[this.link.target_slot];
return this.targetNode.inputs[this.link.target_slot]
}
/**
@@ -46,34 +40,34 @@ export class EzConnection {
* @param { InstanceType<LG["LLink"]> } link
*/
constructor(app, link) {
this.app = app;
this.link = link;
this.app = app
this.link = link
}
disconnect() {
this.targetInput.disconnect();
this.targetInput.disconnect()
}
}
export class EzSlot {
/** @type { EzNode } */
node;
node
/** @type { number } */
index;
index
/**
* @param { EzNode } node
* @param { number } index
*/
constructor(node, index) {
this.node = node;
this.index = index;
this.node = node
this.index = index
}
}
export class EzInput extends EzSlot {
/** @type { INodeInputSlot } */
input;
input
/**
* @param { EzNode } node
@@ -81,26 +75,26 @@ export class EzInput extends EzSlot {
* @param { INodeInputSlot } input
*/
constructor(node, index, input) {
super(node, index);
this.input = input;
super(node, index)
this.input = input
}
get connection() {
const link = this.node.node.inputs?.[this.index]?.link;
const link = this.node.node.inputs?.[this.index]?.link
if (link == null) {
return null;
return null
}
return new EzConnection(this.node.app, this.node.app.graph.links[link]);
return new EzConnection(this.node.app, this.node.app.graph.links[link])
}
disconnect() {
this.node.node.disconnectInput(this.index);
this.node.node.disconnectInput(this.index)
}
}
export class EzOutput extends EzSlot {
/** @type { INodeOutputSlot } */
output;
output
/**
* @param { EzNode } node
@@ -108,21 +102,21 @@ export class EzOutput extends EzSlot {
* @param { INodeOutputSlot } output
*/
constructor(node, index, output) {
super(node, index);
this.output = output;
super(node, index)
this.output = output
}
get connections() {
return (this.node.node.outputs?.[this.index]?.links ?? []).map(
(l) => new EzConnection(this.node.app, this.node.app.graph.links[l])
);
)
}
/**
* @param { EzInput } input
*/
connectTo(input) {
if (!input) throw new Error("Invalid input");
if (!input) throw new Error('Invalid input')
/**
* @type { LG["LLink"] | null }
@@ -131,27 +125,27 @@ export class EzOutput extends EzSlot {
this.index,
input.node.node,
input.index
);
)
if (!link) {
const inp = input.input;
const inName = inp.name || inp.label || inp.type;
const inp = input.input
const inName = inp.name || inp.label || inp.type
throw new Error(
`Connecting from ${input.node.node.type}#${input.node.id}[${inName}#${input.index}] -> ${this.node.node.type}#${this.node.id}[${
this.output.name ?? this.output.type
}#${this.index}] failed.`
);
)
}
return link;
return link
}
}
export class EzNodeMenuItem {
/** @type { EzNode } */
node;
node
/** @type { number } */
index;
index
/** @type { ContextMenuItem } */
item;
item
/**
* @param { EzNode } node
@@ -159,18 +153,18 @@ export class EzNodeMenuItem {
* @param { ContextMenuItem } item
*/
constructor(node, index, item) {
this.node = node;
this.index = index;
this.item = item;
this.node = node
this.index = index
this.item = item
}
call(selectNode = true) {
if (!this.item?.callback)
throw new Error(
`Menu Item ${this.item?.content ?? "[null]"} has no callback.`
);
`Menu Item ${this.item?.content ?? '[null]'} has no callback.`
)
if (selectNode) {
this.node.select();
this.node.select()
}
return this.item.callback.call(
this.node.node,
@@ -179,17 +173,17 @@ export class EzNodeMenuItem {
undefined,
undefined,
this.node.node
);
)
}
}
export class EzWidget {
/** @type { EzNode } */
node;
node
/** @type { number } */
index;
index
/** @type { IWidget } */
widget;
widget
/**
* @param { EzNode } node
@@ -197,104 +191,104 @@ export class EzWidget {
* @param { IWidget } widget
*/
constructor(node, index, widget) {
this.node = node;
this.index = index;
this.widget = widget;
this.node = node
this.index = index
this.widget = widget
}
get value() {
return this.widget.value;
return this.widget.value
}
set value(v) {
this.widget.value = v;
this.widget.callback?.call?.(this.widget, v);
this.widget.value = v
this.widget.callback?.call?.(this.widget, v)
}
get isConvertedToInput() {
// @ts-ignore : this type is valid for converted widgets
return this.widget.type === "converted-widget";
return this.widget.type === 'converted-widget'
}
getConvertedInput() {
if (!this.isConvertedToInput)
throw new Error(`Widget ${this.widget.name} is not converted to input.`);
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
);
(inp) => inp.input['widget']?.name === this.widget.name
)
}
convertToWidget() {
if (!this.isConvertedToInput)
throw new Error(
`Widget ${this.widget.name} cannot be converted as it is already a widget.`
);
var menu = this.node.menu["Convert Input to Widget"].item.submenu.options;
)
var menu = this.node.menu['Convert Input to Widget'].item.submenu.options
var index = menu.findIndex(
(a) => a.content == `Convert ${this.widget.name} to widget`
);
menu[index].callback.call();
)
menu[index].callback.call()
}
convertToInput() {
if (this.isConvertedToInput)
throw new Error(
`Widget ${this.widget.name} cannot be converted as it is already an input.`
);
var menu = this.node.menu["Convert Widget to Input"].item.submenu.options;
)
var menu = this.node.menu['Convert Widget to Input'].item.submenu.options
var index = menu.findIndex(
(a) => a.content == `Convert ${this.widget.name} to input`
);
menu[index].callback.call();
)
menu[index].callback.call()
}
}
export class EzNode {
/** @type { app } */
app;
app
/** @type { LGNode } */
node;
node
/**
* @param { app } app
* @param { LGNode } node
*/
constructor(app, node) {
this.app = app;
this.node = node;
this.app = app
this.node = node
}
get id() {
return this.node.id;
return this.node.id
}
get inputs() {
return this.#makeLookupArray("inputs", "name", EzInput);
return this.#makeLookupArray('inputs', 'name', EzInput)
}
get outputs() {
return this.#makeLookupArray("outputs", "name", EzOutput);
return this.#makeLookupArray('outputs', 'name', EzOutput)
}
get widgets() {
return this.#makeLookupArray("widgets", "name", EzWidget);
return this.#makeLookupArray('widgets', 'name', EzWidget)
}
get menu() {
return this.#makeLookupArray(
() => this.app.canvas.getNodeMenuOptions(this.node),
"content",
'content',
EzNodeMenuItem
);
)
}
get isRemoved() {
return !this.app.graph.getNodeById(this.id);
return !this.app.graph.getNodeById(this.id)
}
select(addToSelection = false) {
this.app.canvas.selectNode(this.node, addToSelection);
this.app.canvas.selectNode(this.node, addToSelection)
}
// /**
@@ -323,60 +317,60 @@ export class EzNode {
*/
#makeLookupArray(nodeProperty, nameProperty, ctor) {
const items =
typeof nodeProperty === "function"
typeof nodeProperty === 'function'
? nodeProperty()
: this.node[nodeProperty];
: this.node[nodeProperty]
// @ts-ignore
return (items ?? []).reduce(
(p, s, i) => {
if (!s) return p;
if (!s) return p
const name = s[nameProperty];
const item = new ctor(this, i, s);
const name = s[nameProperty]
const item = new ctor(this, i, s)
// @ts-ignore
p.push(item);
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;
p[name] = item
return p
},
Object.assign([], { $: this })
);
)
}
}
export class EzGraph {
/** @type { app } */
app;
app
/**
* @param { app } app
*/
constructor(app) {
this.app = app;
this.app = app
}
get nodes() {
return this.app.graph._nodes.map((n) => new EzNode(this.app, n));
return this.app.graph._nodes.map((n) => new EzNode(this.app, n))
}
clear() {
this.app.graph.clear();
this.app.graph.clear()
}
arrange() {
this.app.graph.arrange();
this.app.graph.arrange()
}
stringify() {
return JSON.stringify(this.app.graph.serialize(), undefined);
return JSON.stringify(this.app.graph.serialize(), undefined)
}
/**
@@ -384,36 +378,36 @@ export class EzGraph {
* @returns { EzNode }
*/
find(obj) {
let match;
let id;
if (typeof obj === "number") {
id = obj;
let match
let id
if (typeof obj === 'number') {
id = obj
} else {
id = obj.id;
id = obj.id
}
match = this.app.graph.getNodeById(id);
match = this.app.graph.getNodeById(id)
if (!match) {
throw new Error(`Unable to find node with ID ${id}.`);
throw new Error(`Unable to find node with ID ${id}.`)
}
return new EzNode(this.app, match);
return new EzNode(this.app, match)
}
/**
* @returns { Promise<void> }
*/
reload() {
const graph = JSON.parse(JSON.stringify(this.app.graph.serialize()));
const graph = JSON.parse(JSON.stringify(this.app.graph.serialize()))
return new Promise((r) => {
this.app.graph.clear();
this.app.graph.clear()
setTimeout(async () => {
await this.app.loadGraphData(graph);
await this.app.loadGraphData(graph)
// @ts-ignore
r();
}, 10);
});
r()
}, 10)
})
}
/**
@@ -426,7 +420,7 @@ export class EzGraph {
*/
toPrompt() {
// @ts-ignore
return this.app.graphToPrompt();
return this.app.graphToPrompt()
}
}
@@ -452,10 +446,10 @@ export const Ez = {
*/
graph(app, LiteGraph, LGraphCanvas, clearGraph = true) {
// Always set the active canvas so things work
LGraphCanvas.active_canvas = app.canvas;
LGraphCanvas.active_canvas = app.canvas
if (clearGraph) {
app.graph.clear();
app.graph.clear()
}
// @ts-ignore : this proxy handles utility methods & node creation
@@ -463,35 +457,35 @@ export const Ez = {
{},
{
get(_, p) {
if (typeof p !== "string") throw new Error("Invalid node");
const node = LiteGraph.createNode(p);
if (!node) throw new Error(`Unknown node "${p}"`);
app.graph.add(node);
if (typeof p !== 'string') throw new Error('Invalid node')
const node = LiteGraph.createNode(p)
if (!node) throw new Error(`Unknown node "${p}"`)
app.graph.add(node)
/**
* @param {Parameters<EzNodeFactory>} args
*/
return function (...args) {
const ezNode = new EzNode(app, node);
const inputs = ezNode.inputs;
const ezNode = new EzNode(app, node)
const inputs = ezNode.inputs
let slot = 0;
let slot = 0
for (const arg of args) {
if (arg instanceof EzOutput) {
arg.connectTo(inputs[slot++]);
arg.connectTo(inputs[slot++])
} else {
for (const k in arg) {
ezNode.widgets[k].value = arg[k];
ezNode.widgets[k].value = arg[k]
}
}
}
return ezNode;
};
},
return ezNode
}
}
}
);
)
return { graph: new EzGraph(app), ez: factory };
},
};
return { graph: new EzGraph(app), ez: factory }
}
}

View File

@@ -1,21 +1,21 @@
import { APIConfig, mockApi } from "./setup";
import { Ez, EzGraph, EzNameSpace } from "./ezgraph";
import lg from "./litegraph";
import fs from "fs";
import path from "path";
import { APIConfig, mockApi } from './setup'
import { Ez, EzGraph, EzNameSpace } from './ezgraph'
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;
preSetup?(app): Promise<void>;
localStorage?: Record<string, string>;
resetEnv?: boolean
preSetup?(app): Promise<void>
localStorage?: Record<string, string>
}
interface StartResult {
app: any;
graph: EzGraph;
ez: EzNameSpace;
app: any
graph: EzGraph
ez: EzNameSpace
}
/**
@@ -29,24 +29,24 @@ interface StartResult {
*/
export async function start(config: StartConfig = {}): Promise<StartResult> {
if (config.resetEnv) {
jest.resetModules();
jest.resetAllMocks();
lg.setup(global);
localStorage.clear();
sessionStorage.clear();
jest.resetModules()
jest.resetAllMocks()
lg.setup(global)
localStorage.clear()
sessionStorage.clear()
}
Object.assign(localStorage, config.localStorage ?? {});
document.body.innerHTML = html.toString();
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 };
return { ...Ez.graph(app, LiteGraph, LGraphCanvas), app }
}
/**
@@ -54,9 +54,9 @@ export async function start(config: StartConfig = {}): Promise<StartResult> {
* @param { (hasReloaded: boolean) => (Promise<void> | void) } cb
*/
export async function checkBeforeAndAfterReload(graph, cb) {
await cb(false);
await graph.reload();
await cb(true);
await cb(false)
await graph.reload()
await cb(true)
}
/**
@@ -68,34 +68,34 @@ export async function checkBeforeAndAfterReload(graph, cb) {
export function makeNodeDef(name, input, output = {}) {
const nodeDef = {
name,
category: "test",
category: 'test',
output: [],
output_name: [],
output_is_list: [],
input: {
required: {},
},
};
required: {}
}
}
for (const k in input) {
nodeDef.input.required[k] =
typeof input[k] === "string" ? [input[k], {}] : [...input[k]];
typeof input[k] === 'string' ? [input[k], {}] : [...input[k]]
}
if (output instanceof Array) {
output = output.reduce((p, c) => {
p[c] = c;
return p;
}, {});
p[c] = c
return p
}, {})
}
for (const k in output) {
// @ts-ignore
nodeDef.output.push(output[k]);
nodeDef.output.push(output[k])
// @ts-ignore
nodeDef.output_name.push(k);
nodeDef.output_name.push(k)
// @ts-ignore
nodeDef.output_is_list.push(false);
nodeDef.output_is_list.push(false)
}
return { [name]: nodeDef };
return { [name]: nodeDef }
}
/**
@@ -105,9 +105,9 @@ export function makeNodeDef(name, input, output = {}) {
* @returns { x is Exclude<T, null | undefined> }
*/
export function assertNotNullOrUndefined(x) {
expect(x).not.toEqual(null);
expect(x).not.toEqual(undefined);
return true;
expect(x).not.toEqual(null)
expect(x).not.toEqual(undefined)
return true
}
/**
@@ -116,32 +116,32 @@ export function assertNotNullOrUndefined(x) {
* @param { ReturnType<Ez["graph"]>["graph"] } graph
*/
export function createDefaultWorkflow(ez, graph) {
graph.clear();
const ckpt = ez.CheckpointLoaderSimple();
graph.clear()
const ckpt = ez.CheckpointLoaderSimple()
const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" });
const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" });
const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: 'positive' })
const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: 'negative' })
const empty = ez.EmptyLatentImage();
const empty = ez.EmptyLatentImage()
const sampler = ez.KSampler(
ckpt.outputs.MODEL,
pos.outputs.CONDITIONING,
neg.outputs.CONDITIONING,
empty.outputs.LATENT
);
)
const decode = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE);
const save = ez.SaveImage(decode.outputs.IMAGE);
graph.arrange();
const decode = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE)
const save = ez.SaveImage(decode.outputs.IMAGE)
graph.arrange()
return { ckpt, pos, neg, empty, sampler, decode, save };
return { ckpt, pos, neg, empty, sampler, decode, save }
}
export async function getNodeDefs() {
const { api } = await import("../../src/scripts/api");
return api.getNodeDefs();
const { api } = await import('../../src/scripts/api')
return api.getNodeDefs()
}
export async function getNodeDef(nodeId) {
return (await getNodeDefs())[nodeId];
return (await getNodeDefs())[nodeId]
}

View File

@@ -1,19 +1,19 @@
import fs from "fs";
import path from "path";
import { nop } from "../utils/nopProxy";
import fs from 'fs'
import path from 'path'
import { nop } from '../utils/nopProxy'
function forEachKey(cb) {
for (const k of [
"LiteGraph",
"LGraph",
"LLink",
"LGraphNode",
"LGraphGroup",
"DragAndScale",
"LGraphCanvas",
"ContextMenu",
'LiteGraph',
'LGraph',
'LLink',
'LGraphNode',
'LGraphGroup',
'DragAndScale',
'LGraphCanvas',
'ContextMenu'
]) {
cb(k);
cb(k)
}
}
@@ -24,6 +24,6 @@ export default {
// forEachKey((k) => delete ctx[k]);
// Clear document after each run
document.getElementsByTagName("html")[0].innerHTML = "";
},
};
document.getElementsByTagName('html')[0].innerHTML = ''
}
}

View File

@@ -2,5 +2,5 @@ export const nop = new Proxy(function () {}, {
get: () => nop,
set: () => true,
apply: () => nop,
construct: () => nop,
});
construct: () => nop
})

View File

@@ -1,28 +1,28 @@
import "../../src/scripts/api";
import '../../src/scripts/api'
const fs = require("fs");
const path = require("path");
const fs = require('fs')
const path = require('path')
function* walkSync(dir: string): Generator<string> {
const files = fs.readdirSync(dir, { withFileTypes: true });
const files = fs.readdirSync(dir, { withFileTypes: true })
for (const file of files) {
if (file.isDirectory()) {
yield* walkSync(path.join(dir, file.name));
yield* walkSync(path.join(dir, file.name))
} else {
yield path.join(dir, file.name);
yield path.join(dir, file.name)
}
}
}
export interface APIConfig {
mockExtensions?: string[];
mockNodeDefs?: Record<string, any>;
settings?: Record<string, string>;
mockExtensions?: string[]
mockNodeDefs?: Record<string, any>
settings?: Record<string, string>
userConfig?: {
storage: "server" | "browser";
users?: Record<string, any>;
migrated?: boolean;
};
userData?: Record<string, any>;
storage: 'server' | 'browser'
users?: Record<string, any>
migrated?: boolean
}
userData?: Record<string, any>
}
/**
@@ -42,20 +42,20 @@ export function mockApi(config: APIConfig = {}) {
let { mockExtensions, mockNodeDefs, userConfig, settings, userData } = {
settings: {},
userData: {},
...config,
};
...config
}
if (!mockExtensions) {
mockExtensions = Array.from(walkSync(path.resolve("./src/extensions/core")))
.filter((x) => x.endsWith(".js"))
.map((x) => path.relative(path.resolve("./src/"), x).replace(/\\/g, "/"));
mockExtensions = Array.from(walkSync(path.resolve('./src/extensions/core')))
.filter((x) => x.endsWith('.js'))
.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"))
);
fs.readFileSync(path.resolve('./tests-ui/data/object_info.json'))
)
}
const events = new EventTarget();
const events = new EventTarget()
const mockApi = {
addEventListener: events.addEventListener.bind(events),
removeEventListener: events.removeEventListener.bind(events),
@@ -64,37 +64,37 @@ export function mockApi(config: APIConfig = {}) {
getExtensions: jest.fn(() => mockExtensions),
getNodeDefs: jest.fn(() => mockNodeDefs),
init: jest.fn(),
apiURL: jest.fn((x) => "src/" + x),
fileURL: jest.fn((x) => "src/" + x),
apiURL: jest.fn((x) => 'src/' + x),
fileURL: jest.fn((x) => 'src/' + x),
createUser: jest.fn((username) => {
// @ts-ignore
if (username in userConfig.users) {
return { status: 400, json: () => "Duplicate" };
return { status: 400, json: () => 'Duplicate' }
}
// @ts-ignore
userConfig.users[username + "!"] = username;
return { status: 200, json: () => username + "!" };
userConfig.users[username + '!'] = username
return { status: 200, json: () => username + '!' }
}),
getUserConfig: jest.fn(
() => userConfig ?? { storage: "browser", migrated: false }
() => userConfig ?? { storage: 'browser', migrated: false }
),
getSettings: jest.fn(() => settings),
storeSettings: jest.fn((v) => Object.assign(settings, v)),
getUserData: jest.fn((f) => {
if (f in userData) {
return { status: 200, json: () => userData[f] };
return { status: 200, json: () => userData[f] }
} else {
return { status: 404 };
return { status: 404 }
}
}),
storeUserData: jest.fn((file, data) => {
userData[file] = data;
userData[file] = data
}),
listUserData: jest.fn(() => []),
};
jest.mock("../../src/scripts/api", () => ({
listUserData: jest.fn(() => [])
}
jest.mock('../../src/scripts/api', () => ({
get api() {
return mockApi;
},
}));
return mockApi
}
}))
}