diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml index c6114825b..a49c11dd2 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/test-ui.yaml @@ -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: diff --git a/README.md b/README.md index 12177382b..77ad8e0c8 100644 --- a/README.md +++ b/README.md @@ -471,8 +471,7 @@ navigate to `http://: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 diff --git a/jest.config.fast.ts b/jest.config.fast.ts deleted file mode 100644 index a75d0a63a..000000000 --- a/jest.config.fast.ts +++ /dev/null @@ -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 diff --git a/jest.config.slow.ts b/jest.config.slow.ts deleted file mode 100644 index 169d594f5..000000000 --- a/jest.config.slow.ts +++ /dev/null @@ -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 diff --git a/jest.config.base.ts b/jest.config.ts similarity index 86% rename from jest.config.base.ts rename to jest.config.ts index f4bf6602a..a516e1b5f 100644 --- a/jest.config.base.ts +++ b/jest.config.ts @@ -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 diff --git a/package.json b/package.json index f8ba363c2..dd40988a1 100644 --- a/package.json +++ b/package.json @@ -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/", diff --git a/tests-ui/tests/afterSetup.ts b/tests-ui/tests/afterSetup.ts deleted file mode 100644 index e58966b12..000000000 --- a/tests-ui/tests/afterSetup.ts +++ /dev/null @@ -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) -}) diff --git a/tests-ui/tests/fast/apiTypes.test.ts b/tests-ui/tests/apiTypes.test.ts similarity index 100% rename from tests-ui/tests/fast/apiTypes.test.ts rename to tests-ui/tests/apiTypes.test.ts diff --git a/tests-ui/tests/fast/colorUtil.test.ts b/tests-ui/tests/colorUtil.test.ts similarity index 100% rename from tests-ui/tests/fast/colorUtil.test.ts rename to tests-ui/tests/colorUtil.test.ts diff --git a/tests-ui/tests/fast/comfyWorkflow.test.ts b/tests-ui/tests/comfyWorkflow.test.ts similarity index 100% rename from tests-ui/tests/fast/comfyWorkflow.test.ts rename to tests-ui/tests/comfyWorkflow.test.ts diff --git a/tests-ui/tests/fast/globalSetup.ts b/tests-ui/tests/fast/globalSetup.ts deleted file mode 100644 index 82a8d4f69..000000000 --- a/tests-ui/tests/fast/globalSetup.ts +++ /dev/null @@ -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() - } - }) -} diff --git a/tests-ui/tests/globalSetup.ts b/tests-ui/tests/globalSetup.ts index b9ec5dfbe..12d7908a9 100644 --- a/tests-ui/tests/globalSetup.ts +++ b/tests-ui/tests/globalSetup.ts @@ -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() diff --git a/tests-ui/tests/fast/litegraph.test.ts b/tests-ui/tests/litegraph.test.ts similarity index 100% rename from tests-ui/tests/fast/litegraph.test.ts rename to tests-ui/tests/litegraph.test.ts diff --git a/tests-ui/tests/fast/nodeDef.test.ts b/tests-ui/tests/nodeDef.test.ts similarity index 100% rename from tests-ui/tests/fast/nodeDef.test.ts rename to tests-ui/tests/nodeDef.test.ts diff --git a/tests-ui/tests/fast/nodeSearchService.test.ts b/tests-ui/tests/nodeSearchService.test.ts similarity index 100% rename from tests-ui/tests/fast/nodeSearchService.test.ts rename to tests-ui/tests/nodeSearchService.test.ts diff --git a/tests-ui/tests/fast/nodeSource.test.ts b/tests-ui/tests/nodeSource.test.ts similarity index 100% rename from tests-ui/tests/fast/nodeSource.test.ts rename to tests-ui/tests/nodeSource.test.ts diff --git a/tests-ui/tests/slow/exampleWorkflows.test.ts b/tests-ui/tests/slow/exampleWorkflows.test.ts deleted file mode 100644 index b18ad27ff..000000000 --- a/tests-ui/tests/slow/exampleWorkflows.test.ts +++ /dev/null @@ -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))) - } - ) - } - ) -}) diff --git a/tests-ui/tests/slow/groupNode.test.ts b/tests-ui/tests/slow/groupNode.test.ts deleted file mode 100644 index 5a184c5cd..000000000 --- a/tests-ui/tests/slow/groupNode.test.ts +++ /dev/null @@ -1,1228 +0,0 @@ -// @ts-strict-ignore -import { - start, - createDefaultWorkflow, - getNodeDef, - checkBeforeAndAfterReload -} from '../../utils' -import { EzNode } from '../../utils/ezgraph' -import lg from '../../utils/litegraph' - -describe('group node', () => { - beforeEach(() => { - lg.setup(global) - }) - - afterEach(() => { - lg.teardown(global) - }) - - /** - * - * @param {*} app - * @param {*} graph - * @param {*} name - * @param {*} nodes - * @returns { Promise> } - */ - async function convertToGroup(app, graph, name, nodes) { - // Select the nodes we are converting - for (const n of nodes) { - n.select(true) - } - - expect( - Object.keys(app.canvas.selected_nodes).sort((a, b) => +a - +b) - ).toEqual(nodes.map((n) => n.id + '').sort((a, b) => +a - +b)) - - global.prompt = jest.fn().mockImplementation(() => name) - const groupNode = await nodes[0].menu['Convert to Group Node'].call(false) - - // Check group name was requested - expect(window.prompt).toHaveBeenCalled() - - // Ensure old nodes are removed - for (const n of nodes) { - expect(n.isRemoved).toBeTruthy() - } - - expect(groupNode.type).toEqual('workflow>' + name) - - return graph.find(groupNode) - } - - /** - * @param { Record | number[] } idMap - * @param { Record> } valueMap - */ - function getOutput(idMap = {}, valueMap = {}) { - if (idMap instanceof Array) { - idMap = idMap.reduce((p, n) => { - p[n] = n + '' - return p - }, {}) - } - const expected = { - 1: { - inputs: { ckpt_name: 'model1.safetensors', ...valueMap?.[1] }, - class_type: 'CheckpointLoaderSimple' - }, - 2: { - inputs: { text: 'positive', clip: ['1', 1], ...valueMap?.[2] }, - class_type: 'CLIPTextEncode' - }, - 3: { - inputs: { text: 'negative', clip: ['1', 1], ...valueMap?.[3] }, - class_type: 'CLIPTextEncode' - }, - 4: { - inputs: { width: 512, height: 512, batch_size: 1, ...valueMap?.[4] }, - class_type: 'EmptyLatentImage' - }, - 5: { - inputs: { - seed: 0, - steps: 20, - cfg: 8, - sampler_name: 'euler', - scheduler: 'normal', - denoise: 1, - model: ['1', 0], - positive: ['2', 0], - negative: ['3', 0], - latent_image: ['4', 0], - ...valueMap?.[5] - }, - class_type: 'KSampler' - }, - 6: { - inputs: { samples: ['5', 0], vae: ['1', 2], ...valueMap?.[6] }, - class_type: 'VAEDecode' - }, - 7: { - inputs: { - filename_prefix: 'ComfyUI', - images: ['6', 0], - ...valueMap?.[7] - }, - class_type: 'SaveImage' - } - } - - // Map old IDs to new at the top level - const mapped = {} - for (const oldId in idMap) { - mapped[idMap[oldId]] = expected[oldId] - delete expected[oldId] - } - Object.assign(mapped, expected) - - // Map old IDs to new inside links - for (const k in mapped) { - for (const input in mapped[k].inputs) { - const v = mapped[k].inputs[input] - if (v instanceof Array) { - if (v[0] in idMap) { - v[0] = idMap[v[0]] + '' - } - } - } - } - - return mapped - } - - test('can be created from selected nodes', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - const group = await convertToGroup(app, graph, 'test', [ - nodes.pos, - nodes.neg, - nodes.empty - ]) - - // Ensure links are now to the group node - expect(group.inputs).toHaveLength(2) - expect(group.outputs).toHaveLength(3) - - expect(group.inputs.map((i) => i.input.name)).toEqual([ - 'clip', - 'CLIPTextEncode clip' - ]) - expect(group.outputs.map((i) => i.output.name)).toEqual([ - 'LATENT', - 'CONDITIONING', - 'CLIPTextEncode CONDITIONING' - ]) - - // ckpt clip to both clip inputs on the group - expect( - nodes.ckpt.outputs.CLIP.connections.map((t) => [ - t.targetNode.id, - t.targetInput.index - ]) - ).toEqual([ - [group.id, 0], - [group.id, 1] - ]) - - // group conditioning to sampler - expect( - group.outputs['CONDITIONING'].connections.map((t) => [ - t.targetNode.id, - t.targetInput.index - ]) - ).toEqual([[nodes.sampler.id, 1]]) - // group conditioning 2 to sampler - expect( - group.outputs['CLIPTextEncode CONDITIONING'].connections.map((t) => [ - t.targetNode.id, - t.targetInput.index - ]) - ).toEqual([[nodes.sampler.id, 2]]) - // group latent to sampler - expect( - group.outputs['LATENT'].connections.map((t) => [ - t.targetNode.id, - t.targetInput.index - ]) - ).toEqual([[nodes.sampler.id, 3]]) - }) - - test('maintains all output links on conversion', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - const save2 = ez.SaveImage(...nodes.decode.outputs) - const save3 = ez.SaveImage(...nodes.decode.outputs) - // Ensure an output with multiple links maintains them on convert to group - const group = await convertToGroup(app, graph, 'test', [ - nodes.sampler, - nodes.decode - ]) - expect(group.outputs[0].connections.length).toBe(3) - expect(group.outputs[0].connections[0].targetNode.id).toBe(nodes.save.id) - expect(group.outputs[0].connections[1].targetNode.id).toBe(save2.id) - expect(group.outputs[0].connections[2].targetNode.id).toBe(save3.id) - - // and they're still linked when converting back to nodes - const newNodes = group.menu['Convert to nodes'].call() - const decode = graph.find(newNodes.find((n) => n.type === 'VAEDecode')) - expect(decode.outputs[0].connections.length).toBe(3) - expect(decode.outputs[0].connections[0].targetNode.id).toBe(nodes.save.id) - expect(decode.outputs[0].connections[1].targetNode.id).toBe(save2.id) - expect(decode.outputs[0].connections[2].targetNode.id).toBe(save3.id) - }) - test('can be be converted back to nodes', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - const toConvert = [nodes.pos, nodes.neg, nodes.empty, nodes.sampler] - const group = await convertToGroup(app, graph, 'test', toConvert) - - // Edit some values to ensure they are set back onto the converted nodes - expect(group.widgets['text'].value).toBe('positive') - group.widgets['text'].value = 'pos' - expect(group.widgets['CLIPTextEncode text'].value).toBe('negative') - group.widgets['CLIPTextEncode text'].value = 'neg' - expect(group.widgets['width'].value).toBe(512) - group.widgets['width'].value = 1024 - expect(group.widgets['sampler_name'].value).toBe('euler') - group.widgets['sampler_name'].value = 'ddim' - expect(group.widgets['control_after_generate'].value).toBe('randomize') - group.widgets['control_after_generate'].value = 'fixed' - - /** @type { Array } */ - group.menu['Convert to nodes'].call() - - // ensure widget values are set - const pos = graph.find(nodes.pos.id) - expect(pos.node.type).toBe('CLIPTextEncode') - expect(pos.widgets['text'].value).toBe('pos') - const neg = graph.find(nodes.neg.id) - expect(neg.node.type).toBe('CLIPTextEncode') - expect(neg.widgets['text'].value).toBe('neg') - const empty = graph.find(nodes.empty.id) - expect(empty.node.type).toBe('EmptyLatentImage') - expect(empty.widgets['width'].value).toBe(1024) - const sampler = graph.find(nodes.sampler.id) - expect(sampler.node.type).toBe('KSampler') - expect(sampler.widgets['sampler_name'].value).toBe('ddim') - expect(sampler.widgets['control_after_generate'].value).toBe('fixed') - - // validate links - expect( - nodes.ckpt.outputs.CLIP.connections.map((t) => [ - t.targetNode.id, - t.targetInput.index - ]) - ).toEqual([ - [pos.id, 0], - [neg.id, 0] - ]) - - expect( - pos.outputs['CONDITIONING'].connections.map((t) => [ - t.targetNode.id, - t.targetInput.index - ]) - ).toEqual([[nodes.sampler.id, 1]]) - - expect( - neg.outputs['CONDITIONING'].connections.map((t) => [ - t.targetNode.id, - t.targetInput.index - ]) - ).toEqual([[nodes.sampler.id, 2]]) - - expect( - empty.outputs['LATENT'].connections.map((t) => [ - t.targetNode.id, - t.targetInput.index - ]) - ).toEqual([[nodes.sampler.id, 3]]) - }) - test('it can embed reroutes as inputs', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - - // Add and connect a reroute to the clip text encodes - const reroute = ez.Reroute() - nodes.ckpt.outputs.CLIP.connectTo(reroute.inputs[0]) - reroute.outputs[0].connectTo(nodes.pos.inputs[0]) - reroute.outputs[0].connectTo(nodes.neg.inputs[0]) - - // Convert to group and ensure we only have 1 input of the correct type - const group = await convertToGroup(app, graph, 'test', [ - nodes.pos, - nodes.neg, - nodes.empty, - reroute - ]) - expect(group.inputs).toHaveLength(1) - expect(group.inputs[0].input.type).toEqual('CLIP') - - expect((await graph.toPrompt()).output).toEqual(getOutput()) - }) - test('it can embed reroutes as outputs', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - - // Add a reroute with no output so we output IMAGE even though its used internally - const reroute = ez.Reroute() - nodes.decode.outputs.IMAGE.connectTo(reroute.inputs[0]) - - // Convert to group and ensure there is an IMAGE output - const group = await convertToGroup(app, graph, 'test', [ - nodes.decode, - nodes.save, - reroute - ]) - expect(group.outputs).toHaveLength(1) - expect(group.outputs[0].output.type).toEqual('IMAGE') - expect((await graph.toPrompt()).output).toEqual( - getOutput([nodes.decode.id, nodes.save.id]) - ) - }) - test('it can embed reroutes as pipes', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - - // Use reroutes as a pipe - const rerouteModel = ez.Reroute() - const rerouteClip = ez.Reroute() - const rerouteVae = ez.Reroute() - nodes.ckpt.outputs.MODEL.connectTo(rerouteModel.inputs[0]) - nodes.ckpt.outputs.CLIP.connectTo(rerouteClip.inputs[0]) - nodes.ckpt.outputs.VAE.connectTo(rerouteVae.inputs[0]) - - const group = await convertToGroup(app, graph, 'test', [ - rerouteModel, - rerouteClip, - rerouteVae - ]) - - expect(group.outputs).toHaveLength(3) - expect(group.outputs.map((o) => o.output.type)).toEqual([ - 'MODEL', - 'CLIP', - 'VAE' - ]) - - expect(group.outputs).toHaveLength(3) - expect(group.outputs.map((o) => o.output.type)).toEqual([ - 'MODEL', - 'CLIP', - 'VAE' - ]) - - group.outputs[0].connectTo(nodes.sampler.inputs.model) - group.outputs[1].connectTo(nodes.pos.inputs.clip) - group.outputs[1].connectTo(nodes.neg.inputs.clip) - }) - test('can handle reroutes used internally', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - - const reroutes: EzNode[] = [] - let prevNode = nodes.ckpt - for (let i = 0; i < 5; i++) { - const reroute = ez.Reroute() - prevNode.outputs[0].connectTo(reroute.inputs[0]) - prevNode = reroute - reroutes.push(reroute) - } - prevNode.outputs[0].connectTo(nodes.sampler.inputs.model) - - const group = await convertToGroup(app, graph, 'test', [ - ...reroutes, - ...Object.values(nodes) - ]) - expect((await graph.toPrompt()).output).toEqual(getOutput()) - - group.menu['Convert to nodes'].call() - expect((await graph.toPrompt()).output).toEqual(getOutput()) - }) - test('creates with widget values from inner nodes', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - - nodes.ckpt.widgets.ckpt_name.value = 'model2.ckpt' - nodes.pos.widgets.text.value = 'hello' - nodes.neg.widgets.text.value = 'world' - nodes.empty.widgets.width.value = 256 - nodes.empty.widgets.height.value = 1024 - nodes.sampler.widgets.seed.value = 1 - nodes.sampler.widgets.control_after_generate.value = 'increment' - nodes.sampler.widgets.steps.value = 8 - nodes.sampler.widgets.cfg.value = 4.5 - nodes.sampler.widgets.sampler_name.value = 'uni_pc' - nodes.sampler.widgets.scheduler.value = 'karras' - nodes.sampler.widgets.denoise.value = 0.9 - - const group = await convertToGroup(app, graph, 'test', [ - nodes.ckpt, - nodes.pos, - nodes.neg, - nodes.empty, - nodes.sampler - ]) - - expect(group.widgets['ckpt_name'].value).toEqual('model2.ckpt') - expect(group.widgets['text'].value).toEqual('hello') - expect(group.widgets['CLIPTextEncode text'].value).toEqual('world') - expect(group.widgets['width'].value).toEqual(256) - expect(group.widgets['height'].value).toEqual(1024) - expect(group.widgets['seed'].value).toEqual(1) - expect(group.widgets['control_after_generate'].value).toEqual('increment') - expect(group.widgets['steps'].value).toEqual(8) - expect(group.widgets['cfg'].value).toEqual(4.5) - expect(group.widgets['sampler_name'].value).toEqual('uni_pc') - expect(group.widgets['scheduler'].value).toEqual('karras') - expect(group.widgets['denoise'].value).toEqual(0.9) - - expect((await graph.toPrompt()).output).toEqual( - getOutput( - [ - nodes.ckpt.id, - nodes.pos.id, - nodes.neg.id, - nodes.empty.id, - nodes.sampler.id - ], - { - [nodes.ckpt.id]: { ckpt_name: 'model2.ckpt' }, - [nodes.pos.id]: { text: 'hello' }, - [nodes.neg.id]: { text: 'world' }, - [nodes.empty.id]: { width: 256, height: 1024 }, - [nodes.sampler.id]: { - seed: 1, - steps: 8, - cfg: 4.5, - sampler_name: 'uni_pc', - scheduler: 'karras', - denoise: 0.9 - } - } - ) - ) - }) - test('group inputs can be reroutes', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - const group = await convertToGroup(app, graph, 'test', [ - nodes.pos, - nodes.neg - ]) - - const reroute = ez.Reroute() - nodes.ckpt.outputs.CLIP.connectTo(reroute.inputs[0]) - - reroute.outputs[0].connectTo(group.inputs[0]) - reroute.outputs[0].connectTo(group.inputs[1]) - - expect((await graph.toPrompt()).output).toEqual( - getOutput([nodes.pos.id, nodes.neg.id]) - ) - }) - test('group outputs can be reroutes', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - const group = await convertToGroup(app, graph, 'test', [ - nodes.pos, - nodes.neg - ]) - - const reroute1 = ez.Reroute() - const reroute2 = ez.Reroute() - group.outputs[0].connectTo(reroute1.inputs[0]) - group.outputs[1].connectTo(reroute2.inputs[0]) - - reroute1.outputs[0].connectTo(nodes.sampler.inputs.positive) - reroute2.outputs[0].connectTo(nodes.sampler.inputs.negative) - - expect((await graph.toPrompt()).output).toEqual( - getOutput([nodes.pos.id, nodes.neg.id]) - ) - }) - test('groups can connect to each other', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - const group1 = await convertToGroup(app, graph, 'test', [ - nodes.pos, - nodes.neg - ]) - const group2 = await convertToGroup(app, graph, 'test2', [ - nodes.empty, - nodes.sampler - ]) - - group1.outputs[0].connectTo(group2.inputs['positive']) - group1.outputs[1].connectTo(group2.inputs['negative']) - - expect((await graph.toPrompt()).output).toEqual( - getOutput([nodes.pos.id, nodes.neg.id, nodes.empty.id, nodes.sampler.id]) - ) - }) - test('groups can connect to each other via internal reroutes', async () => { - const { ez, graph, app } = await start() - - const latent = ez.EmptyLatentImage() - const vae = ez.VAELoader() - const latentReroute = ez.Reroute() - const vaeReroute = ez.Reroute() - - latent.outputs[0].connectTo(latentReroute.inputs[0]) - vae.outputs[0].connectTo(vaeReroute.inputs[0]) - - const group1 = await convertToGroup(app, graph, 'test', [ - latentReroute, - vaeReroute - ]) - group1.menu.Clone.call() - expect(app.graph.nodes).toHaveLength(4) - const group2 = graph.find(app.graph.nodes[3]) - expect(group2.node.type).toEqual('workflow>test') - expect(group2.id).not.toEqual(group1.id) - - group1.outputs.VAE.connectTo(group2.inputs.VAE) - group1.outputs.LATENT.connectTo(group2.inputs.LATENT) - - const decode = ez.VAEDecode(group2.outputs.LATENT, group2.outputs.VAE) - const preview = ez.PreviewImage(decode.outputs[0]) - - const output = { - [latent.id]: { - inputs: { width: 512, height: 512, batch_size: 1 }, - class_type: 'EmptyLatentImage' - }, - [vae.id]: { - inputs: { vae_name: 'vae1.safetensors' }, - class_type: 'VAELoader' - }, - [decode.id]: { - inputs: { samples: [latent.id + '', 0], vae: [vae.id + '', 0] }, - class_type: 'VAEDecode' - }, - [preview.id]: { - inputs: { images: [decode.id + '', 0] }, - class_type: 'PreviewImage' - } - } - expect((await graph.toPrompt()).output).toEqual(output) - - // Ensure missing connections dont cause errors - group2.inputs.VAE.disconnect() - delete output[decode.id].inputs.vae - expect((await graph.toPrompt()).output).toEqual(output) - }) - test('displays generated image on group node', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - let group = await convertToGroup(app, graph, 'test', [ - nodes.pos, - nodes.neg, - nodes.empty, - nodes.sampler, - nodes.decode, - nodes.save - ]) - - const { api } = await import('../../../src/scripts/api') - - api.dispatchCustomEvent('execution_start', undefined) - api.dispatchCustomEvent('executing', `${nodes.save.id}`) - // Event should be forwarded to group node id - expect(group.node['imgs']).toBeFalsy() - api.dispatchCustomEvent('executed', { - node: `${nodes.save.id}`, - display_node: `${nodes.save.id}`, - output: { - images: [{ filename: 'test.png', type: 'output' }] - } - }) - - // Trigger paint - group.node.onDrawBackground?.(app.canvas.ctx, app.canvas.canvas) - - expect(group.node['images']).toEqual([ - { filename: 'test.png', type: 'output' } - ]) - - // Reload - const workflow = JSON.stringify((await graph.toPrompt()).workflow) - await app.loadGraphData(JSON.parse(workflow)) - group = graph.find(group) - - // Trigger inner nodes to get created - group.node['getInnerNodes']() - - // Check it works for internal node ids - api.dispatchCustomEvent('execution_start', undefined) - api.dispatchCustomEvent('executing', `${group.id}:5`) - // Event should be forwarded to group node id - expect(group.node['imgs']).toBeFalsy() - api.dispatchCustomEvent('executed', { - node: `${group.id}:5`, - display_node: `${group.id}:5`, - output: { - images: [{ filename: 'test2.png', type: 'output' }] - } - }) - - // Trigger paint - group.node.onDrawBackground?.(app.canvas.ctx, app.canvas.canvas) - - expect(group.node['images']).toEqual([ - { - filename: 'test2.png', - type: 'output' - } - ]) - }) - test('allows widgets to be converted to inputs', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - const group = await convertToGroup(app, graph, 'test', [ - nodes.pos, - nodes.neg - ]) - group.widgets[0].convertToInput() - - const primitive = ez.PrimitiveNode() - primitive.outputs[0].connectTo(group.inputs['text']) - primitive.widgets[0].value = 'hello' - - expect((await graph.toPrompt()).output).toEqual( - getOutput([nodes.pos.id, nodes.neg.id], { - [nodes.pos.id]: { text: 'hello' } - }) - ) - }) - test('can be copied', async () => { - const { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - - const group1 = await convertToGroup(app, graph, 'test', [ - nodes.pos, - nodes.neg, - nodes.empty, - nodes.sampler, - nodes.decode, - nodes.save - ]) - - group1.widgets['text'].value = 'hello' - group1.widgets['width'].value = 256 - group1.widgets['seed'].value = 1 - - // Clone the node - group1.menu.Clone.call() - expect(app.graph.nodes).toHaveLength(3) - const group2 = graph.find(app.graph.nodes[2]) - expect(group2.node.type).toEqual('workflow>test') - expect(group2.id).not.toEqual(group1.id) - - // Reconnect ckpt - nodes.ckpt.outputs.MODEL.connectTo(group2.inputs['model']) - nodes.ckpt.outputs.CLIP.connectTo(group2.inputs['clip']) - nodes.ckpt.outputs.CLIP.connectTo(group2.inputs['CLIPTextEncode clip']) - nodes.ckpt.outputs.VAE.connectTo(group2.inputs['vae']) - - group2.widgets['text'].value = 'world' - group2.widgets['width'].value = 1024 - group2.widgets['seed'].value = 100 - - let i = 0 - expect((await graph.toPrompt()).output).toEqual({ - ...getOutput( - [ - nodes.empty.id, - nodes.pos.id, - nodes.neg.id, - nodes.sampler.id, - nodes.decode.id, - nodes.save.id - ], - { - [nodes.empty.id]: { width: 256 }, - [nodes.pos.id]: { text: 'hello' }, - [nodes.sampler.id]: { seed: 1 } - } - ), - ...getOutput( - { - [nodes.empty.id]: `${group2.id}:${i++}`, - [nodes.pos.id]: `${group2.id}:${i++}`, - [nodes.neg.id]: `${group2.id}:${i++}`, - [nodes.sampler.id]: `${group2.id}:${i++}`, - [nodes.decode.id]: `${group2.id}:${i++}`, - [nodes.save.id]: `${group2.id}:${i++}` - }, - { - [nodes.empty.id]: { width: 1024 }, - [nodes.pos.id]: { text: 'world' }, - [nodes.sampler.id]: { seed: 100 } - } - ) - }) - - graph.arrange() - }) - test('is embedded in workflow', async () => { - let { ez, graph, app } = await start() - const nodes = createDefaultWorkflow(ez, graph) - let group = await convertToGroup(app, graph, 'test', [nodes.pos, nodes.neg]) - const workflow = JSON.stringify((await graph.toPrompt()).workflow) - - // Clear the environment - ;({ ez, graph, app } = await start({ - resetEnv: true - })) - // Ensure the node isnt registered - expect(() => ez['workflow>test']).toThrow() - - // Reload the workflow - await app.loadGraphData(JSON.parse(workflow)) - - // Ensure the node is found - group = graph.find(group) - - // Generate prompt and ensure it is as expected - expect((await graph.toPrompt()).output).toEqual( - getOutput({ - [nodes.pos.id]: `${group.id}:0`, - [nodes.neg.id]: `${group.id}:1` - }) - ) - }) - // Now reports zod validation error - test.skip('shows missing node error on missing internal node when loading graph data', 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: 1, - nodes: [ - { - id: 3, - type: 'workflow>testerror' - } - ], - links: [], - groups: [], - config: {}, - extra: { - groupNodes: { - testerror: { - nodes: [ - { - type: 'NotKSampler' - }, - { - type: 'NotVAEDecode' - } - ] - } - } - } - }) - - expect(dialogShow).toBeCalledTimes(1) - // @ts-expect-error Mocked - const call = dialogShow.mock.calls[0][0].innerHTML - expect(call).toContain('the following node types were not found') - expect(call).toContain('NotKSampler') - expect(call).toContain('NotVAEDecode') - expect(call).toContain('workflow>testerror') - }) - test('maintains widget inputs on conversion back to nodes', async () => { - const { ez, graph, app } = await start() - let pos = ez.CLIPTextEncode({ text: 'positive' }) - pos.node.title = 'Positive' - let neg = ez.CLIPTextEncode({ text: 'negative' }) - neg.node.title = 'Negative' - pos.widgets.text.convertToInput() - neg.widgets.text.convertToInput() - - let primitive = ez.PrimitiveNode() - primitive.outputs[0].connectTo(pos.inputs.text) - primitive.outputs[0].connectTo(neg.inputs.text) - - const group = await convertToGroup(app, graph, 'test', [ - pos, - neg, - primitive - ]) - // This will use a primitive widget named 'value' - expect(group.widgets.length).toBe(1) - expect(group.widgets['value'].value).toBe('positive') - - const newNodes = group.menu['Convert to nodes'].call() - pos = graph.find(newNodes.find((n) => n.title === 'Positive')) - neg = graph.find(newNodes.find((n) => n.title === 'Negative')) - primitive = graph.find(newNodes.find((n) => n.type === 'PrimitiveNode')) - - expect(pos.inputs).toHaveLength(2) - expect(neg.inputs).toHaveLength(2) - expect(primitive.outputs[0].connections).toHaveLength(2) - - expect((await graph.toPrompt()).output).toEqual({ - 1: { inputs: { text: 'positive' }, class_type: 'CLIPTextEncode' }, - 2: { inputs: { text: 'positive' }, class_type: 'CLIPTextEncode' } - }) - }) - test('correctly handles widget inputs', async () => { - const { ez, graph, app } = await start() - const upscaleMethods = (await getNodeDef('ImageScaleBy')).input.required[ - 'upscale_method' - ][0] - - const image = ez.LoadImage() - const scale1 = ez.ImageScaleBy(image.outputs[0]) - const scale2 = ez.ImageScaleBy(image.outputs[0]) - const preview1 = ez.PreviewImage(scale1.outputs[0]) - const preview2 = ez.PreviewImage(scale2.outputs[0]) - scale1.widgets.upscale_method.value = upscaleMethods[1] - scale1.widgets.upscale_method.convertToInput() - - const group = await convertToGroup(app, graph, 'test', [scale1, scale2]) - expect(group.inputs.length).toBe(3) - expect(group.inputs[0].input.type).toBe('IMAGE') - expect(group.inputs[1].input.type).toBe('IMAGE') - expect(group.inputs[2].input.type).toBe('COMBO') - - // Ensure links are maintained - expect(group.inputs[0].connection?.originNode?.id).toBe(image.id) - expect(group.inputs[1].connection?.originNode?.id).toBe(image.id) - expect(group.inputs[2].connection).toBeFalsy() - - // Ensure primitive gets correct type - const primitive = ez.PrimitiveNode() - primitive.outputs[0].connectTo(group.inputs[2]) - expect(primitive.widgets.value.widget.options.values).toBe(upscaleMethods) - expect(primitive.widgets.value.value).toBe(upscaleMethods[1]) // Ensure value is copied - primitive.widgets.value.value = upscaleMethods[1] - - await checkBeforeAndAfterReload(graph, async (r) => { - const scale1id = r ? `${group.id}:0` : scale1.id - const scale2id = r ? `${group.id}:1` : scale2.id - // Ensure widget value is applied to prompt - expect((await graph.toPrompt()).output).toStrictEqual({ - [image.id]: { - inputs: { image: 'example.png', upload: 'image' }, - class_type: 'LoadImage' - }, - [scale1id]: { - inputs: { - upscale_method: upscaleMethods[1], - scale_by: 1, - image: [`${image.id}`, 0] - }, - class_type: 'ImageScaleBy' - }, - [scale2id]: { - inputs: { - upscale_method: 'nearest-exact', - scale_by: 1, - image: [`${image.id}`, 0] - }, - class_type: 'ImageScaleBy' - }, - [preview1.id]: { - inputs: { images: [`${scale1id}`, 0] }, - class_type: 'PreviewImage' - }, - [preview2.id]: { - inputs: { images: [`${scale2id}`, 0] }, - class_type: 'PreviewImage' - } - }) - }) - }) - test('adds widgets in node execution order', async () => { - const { ez, graph, app } = await start() - const scale = ez.LatentUpscale() - const save = ez.SaveImage() - const empty = ez.EmptyLatentImage() - const decode = ez.VAEDecode() - - scale.outputs.LATENT.connectTo(decode.inputs.samples) - decode.outputs.IMAGE.connectTo(save.inputs.images) - empty.outputs.LATENT.connectTo(scale.inputs.samples) - - const group = await convertToGroup(app, graph, 'test', [ - scale, - save, - empty, - decode - ]) - const widgets = group.widgets.map((w) => w.widget.name) - expect(widgets).toStrictEqual([ - 'width', - 'height', - 'batch_size', - 'upscale_method', - 'LatentUpscale width', - 'LatentUpscale height', - 'crop', - 'filename_prefix' - ]) - }) - test('adds output for external links when converting to group', async () => { - const { ez, graph, app } = await start() - const img = ez.EmptyLatentImage() - let decode = ez.VAEDecode(...img.outputs) - const preview1 = ez.PreviewImage(...decode.outputs) - const preview2 = ez.PreviewImage(...decode.outputs) - - const group = await convertToGroup(app, graph, 'test', [ - img, - decode, - preview1 - ]) - - // Ensure we have an output connected to the 2nd preview node - expect(group.outputs.length).toBe(1) - expect(group.outputs[0].connections.length).toBe(1) - expect(group.outputs[0].connections[0].targetNode.id).toBe(preview2.id) - - // Convert back and ensure both previews are still connected - group.menu['Convert to nodes'].call() - decode = graph.find(decode) - expect(decode.outputs[0].connections.length).toBe(2) - expect(decode.outputs[0].connections[0].targetNode.id).toBe(preview1.id) - expect(decode.outputs[0].connections[1].targetNode.id).toBe(preview2.id) - }) - test('adds output for external links when converting to group when nodes are not in execution order', async () => { - const { ez, graph, app } = await start() - const sampler = ez.KSampler() - const ckpt = ez.CheckpointLoaderSimple() - const empty = ez.EmptyLatentImage() - const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: 'positive' }) - const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: 'negative' }) - const decode1 = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE) - const save = ez.SaveImage(decode1.outputs.IMAGE) - ckpt.outputs.MODEL.connectTo(sampler.inputs.model) - pos.outputs.CONDITIONING.connectTo(sampler.inputs.positive) - neg.outputs.CONDITIONING.connectTo(sampler.inputs.negative) - empty.outputs.LATENT.connectTo(sampler.inputs.latent_image) - - const encode = ez.VAEEncode(decode1.outputs.IMAGE) - const vae = ez.VAELoader() - const decode2 = ez.VAEDecode(encode.outputs.LATENT, vae.outputs.VAE) - const preview = ez.PreviewImage(decode2.outputs.IMAGE) - vae.outputs.VAE.connectTo(encode.inputs.vae) - - const group = await convertToGroup(app, graph, 'test', [ - vae, - decode1, - encode, - sampler - ]) - - expect(group.outputs.length).toBe(3) - expect(group.outputs[0].output.name).toBe('VAE') - expect(group.outputs[0].output.type).toBe('VAE') - expect(group.outputs[1].output.name).toBe('IMAGE') - expect(group.outputs[1].output.type).toBe('IMAGE') - expect(group.outputs[2].output.name).toBe('LATENT') - expect(group.outputs[2].output.type).toBe('LATENT') - - expect(group.outputs[0].connections.length).toBe(1) - expect(group.outputs[0].connections[0].targetNode.id).toBe(decode2.id) - expect(group.outputs[0].connections[0].targetInput.index).toBe(1) - - expect(group.outputs[1].connections.length).toBe(1) - expect(group.outputs[1].connections[0].targetNode.id).toBe(save.id) - expect(group.outputs[1].connections[0].targetInput.index).toBe(0) - - expect(group.outputs[2].connections.length).toBe(1) - expect(group.outputs[2].connections[0].targetNode.id).toBe(decode2.id) - expect(group.outputs[2].connections[0].targetInput.index).toBe(0) - - expect((await graph.toPrompt()).output).toEqual({ - ...getOutput({ - 1: ckpt.id, - 2: pos.id, - 3: neg.id, - 4: empty.id, - 5: sampler.id, - 6: decode1.id, - 7: save.id - }), - [vae.id]: { - inputs: { vae_name: 'vae1.safetensors' }, - class_type: vae.node.type - }, - [encode.id]: { - inputs: { pixels: ['6', 0], vae: [vae.id + '', 0] }, - class_type: encode.node.type - }, - [decode2.id]: { - inputs: { samples: [encode.id + '', 0], vae: [vae.id + '', 0] }, - class_type: decode2.node.type - }, - [preview.id]: { - inputs: { images: [decode2.id + '', 0] }, - class_type: preview.node.type - } - }) - }) - test('works with IMAGEUPLOAD widget', async () => { - const { ez, graph, app } = await start() - const img = ez.LoadImage() - const preview1 = ez.PreviewImage(img.outputs[0]) - - const group = await convertToGroup(app, graph, 'test', [img, preview1]) - const widget = group.widgets['upload'] - expect(widget).toBeTruthy() - expect(widget.widget.type).toBe('button') - }) - test('internal primitive populates widgets for all linked inputs', async () => { - const { ez, graph, app } = await start() - const img = ez.LoadImage() - const scale1 = ez.ImageScale(img.outputs[0]) - const scale2 = ez.ImageScale(img.outputs[0]) - ez.PreviewImage(scale1.outputs[0]) - ez.PreviewImage(scale2.outputs[0]) - - scale1.widgets.width.convertToInput() - scale2.widgets.height.convertToInput() - - const primitive = ez.PrimitiveNode() - primitive.outputs[0].connectTo(scale1.inputs.width) - primitive.outputs[0].connectTo(scale2.inputs.height) - - const group = await convertToGroup(app, graph, 'test', [ - img, - primitive, - scale1, - scale2 - ]) - group.widgets.value.value = 100 - expect((await graph.toPrompt()).output).toEqual({ - 1: { - inputs: { image: img.widgets.image.value, upload: 'image' }, - class_type: 'LoadImage' - }, - 2: { - inputs: { - upscale_method: 'nearest-exact', - width: 100, - height: 512, - crop: 'disabled', - image: ['1', 0] - }, - class_type: 'ImageScale' - }, - 3: { - inputs: { - upscale_method: 'nearest-exact', - width: 512, - height: 100, - crop: 'disabled', - image: ['1', 0] - }, - class_type: 'ImageScale' - }, - 4: { inputs: { images: ['2', 0] }, class_type: 'PreviewImage' }, - 5: { inputs: { images: ['3', 0] }, class_type: 'PreviewImage' } - }) - }) - test('primitive control widgets values are copied on convert', async () => { - const { ez, graph, app } = await start() - const sampler = ez.KSampler() - sampler.widgets.seed.convertToInput() - sampler.widgets.sampler_name.convertToInput() - - let p1 = ez.PrimitiveNode() - let p2 = ez.PrimitiveNode() - p1.outputs[0].connectTo(sampler.inputs.seed) - p2.outputs[0].connectTo(sampler.inputs.sampler_name) - - p1.widgets.control_after_generate.value = 'increment' - p2.widgets.control_after_generate.value = 'decrement' - p2.widgets.control_filter_list.value = '/.*/' - - p2.node.title = 'p2' - - const group = await convertToGroup(app, graph, 'test', [sampler, p1, p2]) - expect(group.widgets.control_after_generate.value).toBe('increment') - expect(group.widgets['p2 control_after_generate'].value).toBe('decrement') - expect(group.widgets['p2 control_filter_list'].value).toBe('/.*/') - - group.widgets.control_after_generate.value = 'fixed' - group.widgets['p2 control_after_generate'].value = 'randomize' - group.widgets['p2 control_filter_list'].value = '/.+/' - - group.menu['Convert to nodes'].call() - p1 = graph.find(p1) - p2 = graph.find(p2) - - expect(p1.widgets.control_after_generate.value).toBe('fixed') - expect(p2.widgets.control_after_generate.value).toBe('randomize') - expect(p2.widgets.control_filter_list.value).toBe('/.+/') - }) - test('internal reroutes work with converted inputs and merge options', async () => { - const { ez, graph, app } = await start() - const vae = ez.VAELoader() - const latent = ez.EmptyLatentImage() - const decode = ez.VAEDecode(latent.outputs.LATENT, vae.outputs.VAE) - const scale = ez.ImageScale(decode.outputs.IMAGE) - ez.PreviewImage(scale.outputs.IMAGE) - - const r1 = ez.Reroute() - const r2 = ez.Reroute() - - latent.widgets.width.value = 64 - latent.widgets.height.value = 128 - - latent.widgets.width.convertToInput() - latent.widgets.height.convertToInput() - latent.widgets.batch_size.convertToInput() - - scale.widgets.width.convertToInput() - scale.widgets.height.convertToInput() - - r1.inputs[0].input.label = 'hbw' - r1.outputs[0].connectTo(latent.inputs.height) - r1.outputs[0].connectTo(latent.inputs.batch_size) - r1.outputs[0].connectTo(scale.inputs.width) - - r2.inputs[0].input.label = 'wh' - r2.outputs[0].connectTo(latent.inputs.width) - r2.outputs[0].connectTo(scale.inputs.height) - - const group = await convertToGroup(app, graph, 'test', [ - r1, - r2, - latent, - decode, - scale - ]) - - expect(group.inputs[0].input.type).toBe('VAE') - expect(group.inputs[1].input.type).toBe('INT') - expect(group.inputs[2].input.type).toBe('INT') - - const p1 = ez.PrimitiveNode() - const p2 = ez.PrimitiveNode() - p1.outputs[0].connectTo(group.inputs[1]) - p2.outputs[0].connectTo(group.inputs[2]) - - expect(p1.widgets.value.widget.options?.min).toBe(16) // width/height min - expect(p1.widgets.value.widget.options?.max).toBe(4096) // batch max - expect(p1.widgets.value.widget.options?.step).toBe(80) // width/height step * 10 - - expect(p2.widgets.value.widget.options?.min).toBe(16) // width/height min - expect(p2.widgets.value.widget.options?.max).toBe(16384) // width/height max - expect(p2.widgets.value.widget.options?.step).toBe(80) // width/height step * 10 - - expect(p1.widgets.value.value).toBe(128) - expect(p2.widgets.value.value).toBe(64) - - p1.widgets.value.value = 16 - p2.widgets.value.value = 32 - - await checkBeforeAndAfterReload(graph, async (r) => { - const id = (v) => (r ? `${group.id}:` : '') + v - expect((await graph.toPrompt()).output).toStrictEqual({ - 1: { - inputs: { vae_name: 'vae1.safetensors' }, - class_type: 'VAELoader' - }, - [id(2)]: { - inputs: { width: 32, height: 16, batch_size: 16 }, - class_type: 'EmptyLatentImage' - }, - [id(3)]: { - inputs: { samples: [id(2), 0], vae: ['1', 0] }, - class_type: 'VAEDecode' - }, - [id(4)]: { - inputs: { - upscale_method: 'nearest-exact', - width: 16, - height: 32, - crop: 'disabled', - image: [id(3), 0] - }, - class_type: 'ImageScale' - }, - 5: { inputs: { images: [id(4), 0] }, class_type: 'PreviewImage' } - }) - }) - }) - test('converted inputs with linked widgets map values correctly on creation', async () => { - const { ez, graph, app } = await start() - const k1 = ez.KSampler() - const k2 = ez.KSampler() - k1.widgets.seed.convertToInput() - k2.widgets.seed.convertToInput() - - const rr = ez.Reroute() - rr.outputs[0].connectTo(k1.inputs.seed) - rr.outputs[0].connectTo(k2.inputs.seed) - - const group = await convertToGroup(app, graph, 'test', [k1, k2, rr]) - expect(group.widgets.steps.value).toBe(20) - expect(group.widgets.cfg.value).toBe(8) - expect(group.widgets.scheduler.value).toBe('normal') - expect(group.widgets['KSampler steps'].value).toBe(20) - expect(group.widgets['KSampler cfg'].value).toBe(8) - expect(group.widgets['KSampler scheduler'].value).toBe('normal') - }) - test('allow multiple of the same node type to be added', async () => { - const { ez, graph, app } = await start() - const nodes = [...Array(10)].map(() => ez.ImageScaleBy()) - const group = await convertToGroup(app, graph, 'test', nodes) - expect(group.inputs.length).toBe(10) - expect(group.outputs.length).toBe(10) - expect(group.widgets.length).toBe(20) - expect(group.widgets.map((w) => w.widget.name)).toStrictEqual( - [...Array(10)] - .map((_, i) => `${i > 0 ? 'ImageScaleBy ' : ''}${i > 1 ? i + ' ' : ''}`) - .flatMap((p) => [`${p}upscale_method`, `${p}scale_by`]) - ) - }) -}) diff --git a/tests-ui/tests/slow/widgetInputs.test.ts b/tests-ui/tests/slow/widgetInputs.test.ts deleted file mode 100644 index be76d221e..000000000 --- a/tests-ui/tests/slow/widgetInputs.test.ts +++ /dev/null @@ -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"] } EzNodeFactory - */ - -/** - * @param { EzNodeFactory } ez - * @param { InstanceType } graph - * @param { InstanceType } 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) - }) - }) - }) -}) diff --git a/tests-ui/tests/fast/store/keybindingStore.test.ts b/tests-ui/tests/store/keybindingStore.test.ts similarity index 100% rename from tests-ui/tests/fast/store/keybindingStore.test.ts rename to tests-ui/tests/store/keybindingStore.test.ts diff --git a/tests-ui/tests/fast/store/modelStore.test.ts b/tests-ui/tests/store/modelStore.test.ts similarity index 100% rename from tests-ui/tests/fast/store/modelStore.test.ts rename to tests-ui/tests/store/modelStore.test.ts diff --git a/tests-ui/tests/fast/store/serverConfigStore.test.ts b/tests-ui/tests/store/serverConfigStore.test.ts similarity index 100% rename from tests-ui/tests/fast/store/serverConfigStore.test.ts rename to tests-ui/tests/store/serverConfigStore.test.ts diff --git a/tests-ui/tests/fast/store/settingStore.test.ts b/tests-ui/tests/store/settingStore.test.ts similarity index 100% rename from tests-ui/tests/fast/store/settingStore.test.ts rename to tests-ui/tests/store/settingStore.test.ts diff --git a/tests-ui/tests/fast/store/userFileStore.test.ts b/tests-ui/tests/store/userFileStore.test.ts similarity index 100% rename from tests-ui/tests/fast/store/userFileStore.test.ts rename to tests-ui/tests/store/userFileStore.test.ts diff --git a/tests-ui/tests/fast/store/workflowStore.test.ts b/tests-ui/tests/store/workflowStore.test.ts similarity index 100% rename from tests-ui/tests/fast/store/workflowStore.test.ts rename to tests-ui/tests/store/workflowStore.test.ts diff --git a/tests-ui/tests/fast/utils/treeUtilTest.test.ts b/tests-ui/tests/utils/treeUtilTest.test.ts similarity index 100% rename from tests-ui/tests/fast/utils/treeUtilTest.test.ts rename to tests-ui/tests/utils/treeUtilTest.test.ts