Apply new code format standard (#217)

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

View File

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

View File

@@ -1,121 +1,109 @@
import { parseComfyWorkflow } from "../../src/types/comfyWorkflow";
import { defaultGraph } from "../../src/scripts/defaultGraph";
import fs from "fs";
import { parseComfyWorkflow } from '../../src/types/comfyWorkflow'
import { defaultGraph } from '../../src/scripts/defaultGraph'
import fs from 'fs'
const WORKFLOW_DIR = "tests-ui/workflows";
const WORKFLOW_DIR = 'tests-ui/workflows'
describe("parseComfyWorkflow", () => {
it("parses valid workflow", async () => {
describe('parseComfyWorkflow', () => {
it('parses valid workflow', async () => {
fs.readdirSync(WORKFLOW_DIR).forEach(async (file) => {
if (file.endsWith(".json")) {
const data = fs.readFileSync(`${WORKFLOW_DIR}/${file}`, "utf-8");
await expect(parseComfyWorkflow(data)).resolves.not.toThrow();
if (file.endsWith('.json')) {
const data = fs.readFileSync(`${WORKFLOW_DIR}/${file}`, 'utf-8')
await expect(parseComfyWorkflow(data)).resolves.not.toThrow()
}
});
});
})
})
it("workflow.nodes", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.nodes = undefined;
it('workflow.nodes', async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph))
workflow.nodes = undefined
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow()
workflow.nodes = null
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow()
workflow.nodes = []
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
).resolves.not.toThrow()
})
workflow.nodes = null;
it('workflow.version', async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph))
workflow.version = undefined
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow()
workflow.version = '1.0.1' // Invalid format.
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow()
workflow.version = 1
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
).resolves.not.toThrow()
})
workflow.nodes = [];
it('workflow.extra', async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph))
workflow.extra = undefined
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
});
).resolves.not.toThrow()
it("workflow.version", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.version = undefined;
workflow.extra = null
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
).resolves.not.toThrow()
workflow.version = "1.0.1"; // Invalid format.
workflow.extra = {}
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
).resolves.not.toThrow()
workflow.version = 1;
workflow.extra = { foo: 'bar' } // Should accept extra fields.
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
});
).resolves.not.toThrow()
})
it("workflow.extra", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.extra = undefined;
it('workflow.nodes.pos', async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph))
workflow.nodes[0].pos = [1, 2, 3]
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow()
workflow.nodes[0].pos = [1, 2]
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
workflow.extra = null;
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
workflow.extra = {};
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
workflow.extra = { foo: "bar" }; // Should accept extra fields.
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
});
it("workflow.nodes.pos", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.nodes[0].pos = [1, 2, 3];
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
workflow.nodes[0].pos = [1, 2];
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
).resolves.not.toThrow()
// Should automatically transform the legacy format object to array.
workflow.nodes[0].pos = { "0": 3, "1": 4 };
let parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow));
expect(parsedWorkflow.nodes[0].pos).toEqual([3, 4]);
workflow.nodes[0].pos = { '0': 3, '1': 4 }
let parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow))
expect(parsedWorkflow.nodes[0].pos).toEqual([3, 4])
workflow.nodes[0].pos = { 0: 3, 1: 4 };
parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow));
expect(parsedWorkflow.nodes[0].pos).toEqual([3, 4]);
});
workflow.nodes[0].pos = { 0: 3, 1: 4 }
parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow))
expect(parsedWorkflow.nodes[0].pos).toEqual([3, 4])
})
it("workflow.nodes.widget_values", async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph));
workflow.nodes[0].widgets_values = ["foo", "bar"];
it('workflow.nodes.widget_values', async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph))
workflow.nodes[0].widgets_values = ['foo', 'bar']
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
).resolves.not.toThrow()
workflow.nodes[0].widgets_values = "foo";
workflow.nodes[0].widgets_values = 'foo'
await expect(parseComfyWorkflow(JSON.stringify(workflow))).rejects.toThrow()
workflow.nodes[0].widgets_values = undefined
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).rejects.toThrow();
workflow.nodes[0].widgets_values = undefined;
await expect(
parseComfyWorkflow(JSON.stringify(workflow))
).resolves.not.toThrow();
).resolves.not.toThrow()
// The object format of widgets_values is used by VHS nodes to perform
// dynamic widgets display.
workflow.nodes[0].widgets_values = { foo: "bar" };
const parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow));
expect(parsedWorkflow.nodes[0].widgets_values).toEqual({ foo: "bar" });
});
});
workflow.nodes[0].widgets_values = { foo: 'bar' }
const parsedWorkflow = await parseComfyWorkflow(JSON.stringify(workflow))
expect(parsedWorkflow.nodes[0].widgets_values).toEqual({ foo: 'bar' })
})
})

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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