Remove legacy slow jest tests (#2092)

This commit is contained in:
Chenlei Hu
2024-12-29 16:25:06 -05:00
committed by GitHub
parent 95a4623c37
commit 642094ead1
26 changed files with 4 additions and 2127 deletions

View File

@@ -19,8 +19,7 @@ jobs:
- name: Run Jest tests
run: |
npm run test:generate
npm run test:generate:examples
npm run test:jest:fast -- --verbose
npm run test:jest -- --verbose
working-directory: ComfyUI_frontend
playwright-tests-chromium:

View File

@@ -471,8 +471,7 @@ navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to
- `git clone https://github.com/comfyanonymous/ComfyUI_examples.git` to `tests-ui/ComfyUI_examples` or the EXAMPLE_REPO_PATH location specified in .env
- `npm i` to install all dependencies
- `npm run test:generate` to fetch `tests-ui/data/object_info.json`
- `npm run test:generate:examples` to extract the example workflows
- `npm run test` to execute all unit tests.
- `npm run test:jest` to execute all unit tests.
### Component Test

View File

@@ -1,12 +0,0 @@
import type { JestConfigWithTsJest } from 'ts-jest'
import baseConfig from './jest.config.base'
const jestConfig: JestConfigWithTsJest = {
...baseConfig,
displayName: 'fast',
setupFiles: ['./tests-ui/tests/fast/globalSetup.ts'],
setupFilesAfterEnv: undefined,
testMatch: ['**/tests-ui/tests/fast/**/*.test.ts']
}
export default jestConfig

View File

@@ -1,11 +0,0 @@
import type { JestConfigWithTsJest } from 'ts-jest'
import baseConfig from './jest.config.base'
const jestConfig: JestConfigWithTsJest = {
...baseConfig,
displayName: 'slow (DOM)',
testTimeout: 10000,
testMatch: ['**/tests-ui/tests/slow/**/*.test.ts']
}
export default jestConfig

View File

@@ -21,8 +21,7 @@ const jestConfig: JestConfigWithTsJest = {
},
clearMocks: true,
resetModules: true,
setupFiles: ['./tests-ui/tests/globalSetup.ts'],
setupFilesAfterEnv: ['./tests-ui/tests/afterSetup.ts']
setupFiles: ['./tests-ui/tests/globalSetup.ts']
}
export default jestConfig

View File

@@ -18,10 +18,7 @@
"zipdist": "node scripts/zipdist.js",
"typecheck": "vue-tsc --noEmit && tsc --noEmit && tsc-strict",
"format": "prettier --write './**/*.{js,ts,tsx,vue}'",
"test": "jest --config jest.config.base.ts",
"test:jest:fast": "jest --config jest.config.fast.ts",
"test:jest:slow": "jest --config jest.config.slow.ts",
"test:generate:examples": "npx tsx tests-ui/extractExamples",
"test:jest": "jest --config jest.config.ts",
"test:generate": "npx tsx tests-ui/setup",
"test:browser": "npx playwright test",
"test:component": "vitest run src/components/",

View File

@@ -1,28 +0,0 @@
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 () => {
const originalWarn = console.warn
console.error = function (...args) {
if (['Error: Not implemented: window.alert'].includes(args[0])) {
return
}
}
console.warn = function (...args) {
if (
[
"sMethod=='pointer' && !window.PointerEvent",
'Warning, nodes missing on pasting'
].includes(args[0])
) {
return
}
originalWarn.apply(console, args)
}
console.log = function (...args) {}
lg.setup(global)
await start({ resetEnv: true })
lg.teardown(global)
})

View File

@@ -1,28 +0,0 @@
module.exports = async function () {
jest.mock('@/services/dialogService', () => {
return {
showLoadWorkflowWarning: jest.fn(),
showMissingModelsWarning: jest.fn(),
showSettingsDialog: jest.fn(),
showExecutionErrorDialog: jest.fn(),
showTemplateWorkflowsDialog: jest.fn(),
showPromptDialog: jest
.fn()
.mockImplementation((message, defaultValue) => {
return Promise.resolve(defaultValue)
})
}
})
jest.mock('vue-i18n', () => {
return {
useI18n: jest.fn()
}
})
jest.mock('jsondiffpatch', () => {
return {
diff: jest.fn()
}
})
}

View File

@@ -1,87 +1,4 @@
// @ts-strict-ignore
module.exports = async function () {
global.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
const { nop } = require('../utils/nopProxy')
global.enableWebGLCanvas = nop
global.window.HTMLElement.prototype.hasPointerCapture = jest.fn(() => true)
global.window.HTMLElement.prototype.setPointerCapture = jest.fn()
HTMLCanvasElement.prototype.getContext = nop
localStorage['Comfy.Settings.Comfy.Logging.Enabled'] = 'false'
jest.mock('@/services/dialogService', () => {
return {
showLoadWorkflowWarning: jest.fn(),
showMissingModelsWarning: jest.fn(),
showSettingsDialog: jest.fn(),
showExecutionErrorDialog: jest.fn(),
showTemplateWorkflowsDialog: jest.fn(),
showPromptDialog: jest
.fn()
.mockImplementation((message, defaultValue) => {
return Promise.resolve(defaultValue)
})
}
})
jest.mock('@/stores/toastStore', () => {
return {
useToastStore: () => ({
addAlert: jest.fn()
})
}
})
jest.mock('@/stores/extensionStore', () => {
return {
useExtensionStore: () => ({
registerExtension: jest.fn(),
loadDisabledExtensionNames: jest.fn()
})
}
})
jest.mock('@/stores/workspaceStore', () => {
return {
useWorkspaceStore: () => ({
shiftDown: false,
spinner: false,
focusMode: false,
toggleFocusMode: jest.fn(),
workflow: {
activeWorkflow: null,
syncWorkflows: jest.fn(),
getWorkflowByPath: jest.fn(),
createTemporary: jest.fn(),
openWorkflow: jest.fn()
}
})
}
})
jest.mock('@/stores/workspace/bottomPanelStore', () => {
return {
toggleBottomPanel: jest.fn()
}
})
jest.mock('@/stores/widgetStore', () => {
const widgets = {}
return {
useWidgetStore: () => ({
widgets,
registerCustomWidgets: jest.fn()
})
}
})
jest.mock('vue-i18n', () => {
return {
useI18n: jest.fn()

View File

@@ -1,84 +0,0 @@
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'
// Resolve basic differences in old prompts
function fixLegacyPrompt(prompt: { inputs: any }) {
for (const n of Object.values(prompt)) {
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
}
// 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
}
delete n['is_changed']
}
return prompt
}
describe('example workflows', () => {
beforeEach(() => {
lg.setup(global)
})
afterEach(() => {
lg.teardown(global)
})
const workflowFiles = readdirSync(WORKFLOW_DIR).filter((file) =>
file.endsWith('.json')
)
const workflows = workflowFiles.map((file) => {
const { workflow, prompt } = JSON.parse(
readFileSync(path.resolve(WORKFLOW_DIR, file), 'utf8')
)
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
} catch (error) {}
// https://github.com/comfyanonymous/ComfyUI_examples/issues/40
if (file === 'audio_stable_audio_example.flac.json') {
skip = true
}
return { file, workflow, prompt, parsedWorkflow, skip }
})
describe.each(workflows)(
'Workflow Test: %s',
({ file, workflow, prompt, parsedWorkflow, skip }) => {
;(skip ? test.skip : test)(
'correctly generates prompt json for ' + file,
async () => {
if (!workflow || !prompt) throw new Error('Invalid example json')
const { app } = await start()
await app.loadGraphData(parsedWorkflow)
const output = await app.graphToPrompt()
expect(output.output).toEqual(fixLegacyPrompt(JSON.parse(prompt)))
}
)
}
)
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,643 +0,0 @@
// @ts-strict-ignore
import {
start,
makeNodeDef,
checkBeforeAndAfterReload,
assertNotNullOrUndefined,
createDefaultWorkflow
} from '../../utils'
import lg from '../../utils/litegraph'
/**
* @typedef { import("../utils/ezgraph") } Ez
* @typedef { ReturnType<Ez["Ez"]["graph"]>["ez"] } EzNodeFactory
*/
/**
* @param { EzNodeFactory } ez
* @param { InstanceType<Ez["EzGraph"]> } graph
* @param { InstanceType<Ez["EzInput"]> } input
* @param { string } widgetType
* @param { number } controlWidgetCount
* @returns
*/
async function connectPrimitiveAndReload(
ez,
graph,
input,
widgetType,
controlWidgetCount = 0
) {
// Connect to primitive and ensure its still connected after
let primitive = ez.PrimitiveNode()
primitive.outputs[0].connectTo(input)
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)
// Ensure widget is correct type
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')
}
}
// Ensure we dont have other widgets
expect(primitive.node.widgets).toHaveLength(1 + controlWidgetCount)
})
return primitive
}
describe('widget inputs', () => {
beforeEach(() => {
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' },
{
name: 'customtext',
type: 'STRING',
opt: { multiline: true }
},
{ 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 ?? {}]
})
})
// 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()
await connectPrimitiveAndReload(
ez,
graph,
input,
c.widget ?? c.name,
c.control
)
})
})
test('converted widget works after reload', async () => {
const { ez, graph } = await start()
let n = ez.CheckpointLoaderSimple()
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)
// 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)
// Convert again and reload the graph to ensure it maintains state
n.widgets.ckpt_name.convertToInput()
expect(n.inputs.length).toEqual(inputCount + 1)
const primitive = await connectPrimitiveAndReload(
ez,
graph,
n.inputs.ckpt_name,
'combo',
2
)
// Disconnect & reconnect
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)
// 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)
})
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()
// Clone the node
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()
// 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)
// 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()
})
// Invalid workflow against zod schema now.
test.skip('shows missing node error on custom node with converted input', async () => {
const { graph } = await start()
const dialogShow = jest.spyOn(graph.app.ui.dialog, 'show')
await graph.app.loadGraphData({
last_node_id: 3,
last_link_id: 4,
nodes: [
{
id: 1,
type: 'TestNode',
pos: [41.87329101561909, 389.7381480823742],
size: { 0: 220, 1: 374 },
flags: {},
order: 1,
mode: 0,
inputs: [
{
name: 'test',
type: 'FLOAT',
link: 4,
widget: { name: 'test' },
slot_index: 0
}
],
outputs: [],
properties: { 'Node name for S&R': 'TestNode' },
widgets_values: [1]
},
{
id: 3,
type: 'PrimitiveNode',
pos: [-312, 433],
size: { 0: 210, 1: 82 },
flags: {},
order: 0,
mode: 0,
// Missing name and type
outputs: [{ links: [4], widget: { name: 'test' } }],
title: 'test',
properties: {}
}
],
links: [[4, 3, 0, 1, 6, 'FLOAT']],
groups: [],
config: {},
extra: {},
version: 0.4
})
expect(dialogShow).toBeCalledTimes(1)
// @ts-expect-error
expect(dialogShow.mock.calls[0][0].innerHTML).toContain(
'the following node types were not found'
)
// @ts-expect-error
expect(dialogShow.mock.calls[0][0].innerHTML).toContain('TestNode')
})
test('defaultInput widgets can be converted back to inputs', async () => {
const { graph, ez } = await start({
mockNodeDefs: makeNodeDef('TestNode', {
example: ['INT', { defaultInput: true }]
})
})
// 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()
// Ensure it can be converted to
w.convertToWidget()
expect(w.isConvertedToInput).toBeFalsy()
expect(n.inputs.length).toEqual(0)
// and from
w.convertToInput()
expect(w.isConvertedToInput).toBeTruthy()
input = w.getConvertedInput()
// Reload and ensure it still only has 1 converted widget
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()
// 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)
})
test('forceInput widgets can not be converted back to inputs', async () => {
const { graph, ez } = await start({
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()
// Convert to widget should error
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()
}
})
test('primitive can connect to matching combos on converted widgets', async () => {
const { ez } = await start({
mockNodeDefs: {
...makeNodeDef('TestNode1', {
example: [['A', 'B', 'C'], { forceInput: true }]
}),
...makeNodeDef('TestNode2', {
example: [['A', 'B', 'C'], { forceInput: true }]
})
}
})
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 () => {
const { ez } = await start({
mockNodeDefs: {
...makeNodeDef('TestNode1', {
example: [['A', 'B', 'C'], { 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)
})
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('TestNode3', {
example: [['A', 'B', 'C'], { forceInput: true }]
})
}
})
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()
})
test('combo primitive can filter list when control_after_generate called', async () => {
const { ez } = await start({
mockNodeDefs: {
...makeNodeDef('TestNode1', {
example: [
['A', 'B', 'C', 'D', 'AA', 'BB', 'CC', 'DD', 'AAA', 'BBB'],
{}
]
})
}
})
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
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')
// Filter to items containing D
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')
// 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')
// Check random
control.value = 'randomize'
filter.value = '/D/'
for (let i = 0; i < 100; i++) {
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')
})
describe('reroutes', () => {
async function checkOutput(graph, values) {
expect((await graph.toPrompt()).output).toStrictEqual({
1: {
inputs: { ckpt_name: 'model1.safetensors' },
class_type: 'CheckpointLoaderSimple'
},
2: {
inputs: { text: 'positive', clip: ['1', 1] },
class_type: 'CLIPTextEncode'
},
3: {
inputs: { text: 'negative', clip: ['1', 1] },
class_type: 'CLIPTextEncode'
},
4: {
inputs: {
width: values.width ?? 512,
height: values.height ?? 512,
batch_size: values?.batch_size ?? 1
},
class_type: 'EmptyLatentImage'
},
5: {
inputs: {
seed: 0,
steps: 20,
cfg: 8,
sampler_name: 'euler',
scheduler: values?.scheduler ?? 'normal',
denoise: 1,
model: ['1', 0],
positive: ['2', 0],
negative: ['3', 0],
latent_image: ['4', 0]
},
class_type: 'KSampler'
},
6: {
inputs: { samples: ['5', 0], vae: ['1', 2] },
class_type: 'VAEDecode'
},
7: {
inputs: {
filename_prefix: values.filename_prefix ?? 'ComfyUI',
images: ['6', 0]
},
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))
if (node.widgets?.value) {
return
}
}
}
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()
let widthReroute = ez.Reroute()
let schedulerReroute = ez.Reroute()
let fileReroute = ez.Reroute()
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
next = ez.Reroute()
schedulerNext.outputs[0].connectTo(next.inputs[0])
schedulerNext = 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)
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'
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)
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)
nodes.empty.widgets.width.convertToInput()
nodes.empty.widgets.height.convertToInput()
nodes.empty.widgets.batch_size.convertToInput()
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 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)
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)
// 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
await checkOutput(graph, {
width: 64,
height: 64,
batch_size: 64
})
})
})
it('primitive maintains size on reload', async () => {
const { ez, graph } = await start()
const primitive = ez.PrimitiveNode()
const image = ez.EmptyImage()
image.widgets.width.convertToInput()
primitive.outputs[0].connectTo(image.inputs.width)
primitive.node.size = [999, 999]
await checkBeforeAndAfterReload(graph, async (r) => {
const { node } = graph.find(primitive)
expect(node.size[0]).toBe(999)
expect(node.size[1]).toBe(999)
})
})
})
})