mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 02:02:08 +00:00
Apply new code format standard (#217)
This commit is contained in:
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
);
|
||||
)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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 = ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ export const nop = new Proxy(function () {}, {
|
||||
get: () => nop,
|
||||
set: () => true,
|
||||
apply: () => nop,
|
||||
construct: () => nop,
|
||||
});
|
||||
construct: () => nop
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user