Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled

Includes 30 custom nodes committed directly, 7 Civitai-exclusive
loras stored via Git LFS, and a setup script that installs all
dependencies and downloads HuggingFace-hosted models on vast.ai.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 00:55:26 +00:00
parent 2b70ab9ad0
commit f09734b0ee
2274 changed files with 748556 additions and 3 deletions

View File

@@ -0,0 +1,71 @@
import type {LGraphNode} from "@comfyorg/frontend";
import {app} from "scripts/app.js";
import {wait} from "rgthree/common/shared_utils.js";
import {NodeTypesString} from "../constants.js";
type addNodeOptions = {
placement?: string;
};
/**
* A testing environment to make setting up, clearing, and queuing more predictable in an
* integration test environment.
*/
export class ComfyUITestEnvironment {
private lastNode: LGraphNode | null = null;
private maxY = 0;
constructor() {}
wait = wait;
async addNode(nodeString: string, options: addNodeOptions = {}) {
const [canvas, graph] = [app.canvas, app.graph];
const node = LiteGraph.createNode(nodeString)!;
let x = 0;
let y = 30;
if (this.lastNode) {
const placement = options.placement || "right";
if (placement === "under") {
x = this.lastNode.pos[0];
y = this.lastNode.pos[1] + this.lastNode.size[1] + 30;
} else if (placement === "right") {
x = this.lastNode.pos[0] + this.lastNode.size[0] + 100;
y = this.lastNode.pos[1];
} else if (placement === "start") {
x = 0;
y = this.maxY + 50;
}
}
canvas.graph!.add(node);
node.pos = [x, y];
canvas.selectNode(node);
app.graph.setDirtyCanvas(true, true);
await wait();
this.lastNode = node;
this.maxY = Math.max(this.maxY, y + this.lastNode.size[1]);
return (this.lastNode = node);
}
async clear() {
app.clean();
app.graph.clear();
const nodeConfig = await this.addNode(NodeTypesString.KSAMPLER_CONFIG);
const displayAny = await this.addNode(NodeTypesString.DISPLAY_ANY);
nodeConfig.widgets = nodeConfig.widgets || [];
nodeConfig.widgets[0]!.value = Math.round(Math.random() * 100);
nodeConfig.connect(0, displayAny, 0);
await this.queuePrompt();
app.clean();
app.graph.clear();
this.lastNode = null;
this.maxY = 0;
await wait();
}
async queuePrompt() {
await app.queuePrompt(0);
await wait(150);
}
}

View File

@@ -0,0 +1,167 @@
/**
* @fileoverview A set of methods that mimic a bit of the Jasmine testing library, but simpler and
* more succinct for manipulating a comfy integration test.
*
* Tests are not bundled by default, to test build with "--with-tests" and then invoke from the
* dev console like `rgthree_tests.TestDescribeLabel()`. The output is in the test itself.
*/
import { wait } from "rgthree/common/shared_utils.js";
declare global {
interface Window {
rgthree_tests: {
[key: string]: any;
};
}
}
window.rgthree_tests = window.rgthree_tests || {};
type TestContext = {
label?: string;
beforeEach?: Function[];
};
let contexts: TestContext[] = [];
export function describe(label: string, fn: Function) {
if (!label.startsWith('Test')) {
throw new Error('Test labels should start with "Test"');
}
window.rgthree_tests[label] = async () => {
await describeRun(label, fn);
};
return window.rgthree_tests[label];
}
export async function describeRun(label: string, fn: Function) {
await wait();
contexts.push({ label });
console.group(`[Start] ${contexts[contexts.length - 1]!.label}`);
await fn();
contexts.pop();
console.groupEnd();
}
export async function should(declaration: string, fn: Function) {
if (!contexts[contexts.length - 1]) {
throw Error("Called should outside of a describe.");
}
console.group(`...should ${declaration}`);
try {
for (const context of contexts) {
for (const beforeEachFn of context?.beforeEach || []) {
await beforeEachFn();
}
}
await fn();
} catch (e: any) {
fail(e);
}
console.groupEnd();
}
export async function beforeEach(fn: Function) {
if (!contexts[contexts.length - 1]) {
throw Error("Called beforeEach outside of a describe.");
}
const last = contexts[contexts.length - 1]!;
last.beforeEach = last?.beforeEach || [];
last.beforeEach.push(fn);
}
export function fail(e: Error) {
log(`X Failure: ${e}`, "color:#600; background:#fdd; padding: 2px 6px;");
}
function log(msg: string, styles: string) {
if (styles) {
console.log(`%c ${msg}`, styles);
} else {
console.log(msg);
}
}
class Expectation {
private propertyLabel: string | null = "";
private expectedLabel: string | null = "";
private verbLabel: string | null = "be";
private expectedFn!: (v: any) => boolean;
private value: any;
constructor(value: any) {
this.value = value;
}
toBe(labelOrExpected: any, maybeExpected?: any) {
const expected = maybeExpected !== undefined ? maybeExpected : labelOrExpected;
this.propertyLabel = maybeExpected !== undefined ? labelOrExpected : null;
this.expectedLabel = JSON.stringify(expected);
this.expectedFn = (v) => v == expected;
return this.toBeEval();
}
toMatchJson(labelOrExpected: any, maybeExpected?: any) {
const expected = maybeExpected !== undefined ? maybeExpected : labelOrExpected;
this.propertyLabel = maybeExpected !== undefined ? labelOrExpected : null;
this.expectedLabel = JSON.stringify(expected);
this.expectedFn = (v) => JSON.stringify(JSON.parse(v)) == JSON.stringify(JSON.parse(expected));
return this.toBeEval();
}
toBeUndefined(propertyLabel: string) {
this.expectedFn = (v) => v === undefined;
this.propertyLabel = propertyLabel || "";
this.expectedLabel = "undefined";
return this.toBeEval(true);
}
toBeNullOrUndefined(propertyLabel: string) {
this.expectedFn = (v) => v == null;
this.propertyLabel = propertyLabel || "";
this.expectedLabel = "null or undefined";
return this.toBeEval(true);
}
toBeTruthy(propertyLabel: string) {
this.expectedFn = (v) => !v;
this.propertyLabel = propertyLabel || "";
this.expectedLabel = "truthy";
return this.toBeEval(false);
}
toBeANumber(propertyLabel: string) {
this.expectedFn = (v) => typeof v === "number";
this.propertyLabel = propertyLabel || "";
this.expectedLabel = "a number";
return this.toBeEval();
}
toContain(labelOrExpected: any, maybeExpected?: any) {
const expected = maybeExpected !== undefined ? maybeExpected : labelOrExpected;
this.propertyLabel = maybeExpected !== undefined ? labelOrExpected : null;
this.verbLabel = 'contain';
this.expectedLabel = JSON.stringify(expected);
this.expectedFn = (v) => v.includes(expected);
return this.toBeEval();
}
toBeEval(strict = false) {
let evaluation = this.expectedFn(this.value);
let msg = `Expected ${this.propertyLabel ? this.propertyLabel + ` to ${this.verbLabel} ` : ""}${
this.expectedLabel
}`;
msg += evaluation ? "." : `, but was ${JSON.stringify(this.value)}`;
this.log(evaluation, msg);
return evaluation;
}
log(value: boolean, msg: string) {
if (value) {
log(`🗸 ${msg}`, "color:#060; background:#cec; padding: 2px 6px;");
} else {
log(`X ${msg}`, "color:#600; background:#fdd; padding: 2px 6px;");
}
}
}
export function expect(value: any, msg?: string) {
const expectation = new Expectation(value);
if (msg) {
expectation.log(value, msg);
}
return expectation;
}

View File

@@ -0,0 +1,41 @@
import type {LGraphNode} from "@comfyorg/frontend";
import type {ComfyUITestEnvironment} from "./comfyui_env";
export const PNG_1x1 =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P4v5ThPwAG7wKklwQ/bwAAAABJRU5ErkJggg==";
export const PNG_1x2 =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEElEQVQIW2NgYGD4D8QM/wEHAwH/OMSHKAAAAABJRU5ErkJggg==";
export const PNG_2x1 =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAAD0lEQVQIW2NkYGD4D8QMAAUNAQFqjhCLAAAAAElFTkSuQmCC";
export async function pasteImageToLoadImageNode(
env: ComfyUITestEnvironment,
dataUrl?: string,
node?: LGraphNode,
) : Promise<LGraphNode> {
const dataArr = (dataUrl ?? PNG_1x1).split(",");
const mime = dataArr[0]!.match(/:(.*?);/)![1];
const bstr = atob(dataArr[1]!);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const filename = `test_image_${+new Date()}.png`;
const file = new File([u8arr], filename, {type: mime});
if (!node) {
node = await env.addNode("LoadImage");
}
await (node as any).pasteFiles([file]);
let i = 0;
let good = false;
while (i++ < 10 || good) {
good = node.widgets![0]!.value === filename;
if (good) break;
await env.wait(100);
}
if (!good) {
throw new Error("Expected file not loaded.");
}
return node;
}