diff --git a/browser_tests/globalSetup.ts b/browser_tests/globalSetup.ts
index 881ef11c43..da84489423 100644
--- a/browser_tests/globalSetup.ts
+++ b/browser_tests/globalSetup.ts
@@ -1,17 +1,20 @@
import type { FullConfig } from '@playwright/test'
-import dotenv from 'dotenv'
+import { config as loadEnv } from 'dotenv'
import { backupPath } from './utils/backupUtils'
+import { syncDevtools } from './utils/devtoolsSync'
-dotenv.config()
+loadEnv()
-export default function globalSetup(config: FullConfig) {
+export default function globalSetup(_: FullConfig) {
if (!process.env.CI) {
if (process.env.TEST_COMFYUI_DIR) {
backupPath([process.env.TEST_COMFYUI_DIR, 'user'])
backupPath([process.env.TEST_COMFYUI_DIR, 'models'], {
renameAndReplaceWithScaffolding: true
})
+
+ syncDevtools(process.env.TEST_COMFYUI_DIR)
} else {
console.warn(
'Set TEST_COMFYUI_DIR in .env to prevent user data (settings, workflows, etc.) from being overwritten'
diff --git a/browser_tests/utils/devtoolsSync.ts b/browser_tests/utils/devtoolsSync.ts
new file mode 100644
index 0000000000..4e9549c95b
--- /dev/null
+++ b/browser_tests/utils/devtoolsSync.ts
@@ -0,0 +1,52 @@
+import fs from 'fs-extra'
+import path from 'path'
+import { fileURLToPath } from 'url'
+
+export function syncDevtools(targetComfyDir: string): boolean {
+ if (!targetComfyDir) {
+ console.warn('syncDevtools skipped: TEST_COMFYUI_DIR not set')
+ return false
+ }
+
+ // Validate and sanitize the target directory path
+ const resolvedTargetDir = path.resolve(targetComfyDir)
+
+ // Basic path validation to prevent directory traversal
+ if (resolvedTargetDir.includes('..') || !path.isAbsolute(resolvedTargetDir)) {
+ console.error('syncDevtools failed: Invalid target directory path')
+ return false
+ }
+
+ const moduleDir =
+ typeof __dirname !== 'undefined'
+ ? __dirname
+ : path.dirname(fileURLToPath(import.meta.url))
+
+ const devtoolsSrc = path.resolve(moduleDir, '..', '..', 'tools', 'devtools')
+
+ if (!fs.pathExistsSync(devtoolsSrc)) {
+ console.warn(
+ `syncDevtools skipped: source directory not found at ${devtoolsSrc}`
+ )
+ return false
+ }
+
+ const devtoolsDest = path.resolve(
+ resolvedTargetDir,
+ 'custom_nodes',
+ 'ComfyUI_devtools'
+ )
+
+ console.warn(`syncDevtools: copying ${devtoolsSrc} -> ${devtoolsDest}`)
+
+ try {
+ fs.removeSync(devtoolsDest)
+ fs.ensureDirSync(devtoolsDest)
+ fs.copySync(devtoolsSrc, devtoolsDest, { overwrite: true })
+ console.warn('syncDevtools: copy complete')
+ return true
+ } catch (error) {
+ console.error(`Failed to sync DevTools to ${devtoolsDest}:`, error)
+ return false
+ }
+}
diff --git a/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue b/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue
index f4a9a4c358..55f5b82f93 100644
--- a/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue
+++ b/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue
@@ -46,11 +46,11 @@
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.test.ts
index f0fbff926a..1c9d4f0556 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.test.ts
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.test.ts
@@ -5,11 +5,12 @@ import type { MultiSelectProps } from 'primevue/multiselect'
import { describe, expect, it } from 'vitest'
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
+import { createMockWidget } from '../testUtils'
import WidgetMultiSelect from './WidgetMultiSelect.vue'
describe('WidgetMultiSelect Value Binding', () => {
- const createMockWidget = (
+ const createLocalMockWidget = (
value: WidgetValue[] = [],
options: Partial & { values?: WidgetValue[] } = {},
callback?: (value: WidgetValue[]) => void
@@ -50,9 +51,17 @@ describe('WidgetMultiSelect Value Binding', () => {
describe('Vue Event Emission', () => {
it('emits Vue event when selection changes', async () => {
- const widget = createMockWidget([], {
- values: ['option1', 'option2', 'option3']
- })
+ const widget = createMockWidget(
+ [],
+ {},
+ undefined,
+ {},
+ {
+ type: 'MULTISELECT',
+ name: 'test_widget',
+ options: { values: ['option1', 'option2', 'option3'] }
+ }
+ )
const wrapper = mountComponent(widget, [])
await setMultiSelectValueAndEmit(wrapper, ['option1', 'option2'])
@@ -63,9 +72,17 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('emits Vue event when selection is cleared', async () => {
- const widget = createMockWidget(['option1'], {
- values: ['option1', 'option2']
- })
+ const widget = createMockWidget(
+ ['option1'],
+ {},
+ undefined,
+ {},
+ {
+ type: 'MULTISELECT',
+ name: 'test_widget',
+ options: { values: ['option1', 'option2'] }
+ }
+ )
const wrapper = mountComponent(widget, ['option1'])
await setMultiSelectValueAndEmit(wrapper, [])
@@ -76,7 +93,7 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('handles single item selection', async () => {
- const widget = createMockWidget([], {
+ const widget = createLocalMockWidget([], {
values: ['single']
})
const wrapper = mountComponent(widget, [])
@@ -89,7 +106,7 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('emits update:modelValue for callback handling at parent level', async () => {
- const widget = createMockWidget([], {
+ const widget = createLocalMockWidget([], {
values: ['option1', 'option2']
})
const wrapper = mountComponent(widget, [])
@@ -103,7 +120,7 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('handles missing callback gracefully', async () => {
- const widget = createMockWidget(
+ const widget = createLocalMockWidget(
[],
{
values: ['option1']
@@ -123,7 +140,7 @@ describe('WidgetMultiSelect Value Binding', () => {
describe('Component Rendering', () => {
it('renders multiselect component', () => {
- const widget = createMockWidget([], {
+ const widget = createLocalMockWidget([], {
values: ['option1', 'option2']
})
const wrapper = mountComponent(widget, [])
@@ -134,7 +151,17 @@ describe('WidgetMultiSelect Value Binding', () => {
it('displays options from widget values', () => {
const options = ['apple', 'banana', 'cherry']
- const widget = createMockWidget([], { values: options })
+ const widget = createMockWidget(
+ [],
+ {},
+ undefined,
+ {},
+ {
+ type: 'MULTISELECT',
+ name: 'test_widget',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, [])
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
@@ -142,9 +169,17 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('displays initial selected values', () => {
- const widget = createMockWidget(['banana'], {
- values: ['apple', 'banana', 'cherry']
- })
+ const widget = createMockWidget(
+ ['banana'],
+ {},
+ undefined,
+ {},
+ {
+ type: 'MULTISELECT',
+ name: 'test_widget',
+ options: { values: ['apple', 'banana', 'cherry'] }
+ }
+ )
const wrapper = mountComponent(widget, ['banana'])
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
@@ -152,7 +187,17 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('applies small size styling', () => {
- const widget = createMockWidget([], { values: ['test'] })
+ const widget = createMockWidget(
+ [],
+ {},
+ undefined,
+ {},
+ {
+ type: 'MULTISELECT',
+ name: 'test_widget',
+ options: { values: ['test'] }
+ }
+ )
const wrapper = mountComponent(widget, [])
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
@@ -160,7 +205,17 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('uses chip display mode', () => {
- const widget = createMockWidget([], { values: ['test'] })
+ const widget = createMockWidget(
+ [],
+ {},
+ undefined,
+ {},
+ {
+ type: 'MULTISELECT',
+ name: 'test_widget',
+ options: { values: ['test'] }
+ }
+ )
const wrapper = mountComponent(widget, [])
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
@@ -168,7 +223,17 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('applies text-xs class', () => {
- const widget = createMockWidget([], { values: ['test'] })
+ const widget = createMockWidget(
+ [],
+ {},
+ undefined,
+ {},
+ {
+ type: 'MULTISELECT',
+ name: 'test_widget',
+ options: { values: ['test'] }
+ }
+ )
const wrapper = mountComponent(widget, [])
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
@@ -178,7 +243,7 @@ describe('WidgetMultiSelect Value Binding', () => {
describe('Widget Options Handling', () => {
it('passes through valid widget options', () => {
- const widget = createMockWidget([], {
+ const widget = createLocalMockWidget([], {
values: ['option1', 'option2'],
placeholder: 'Select items...',
filter: true,
@@ -193,7 +258,7 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('excludes panel-related props', () => {
- const widget = createMockWidget([], {
+ const widget = createLocalMockWidget([], {
values: ['option1'],
overlayStyle: { color: 'red' },
panelClass: 'custom-panel'
@@ -207,7 +272,17 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('handles empty values array', () => {
- const widget = createMockWidget([], { values: [] })
+ const widget = createMockWidget(
+ [],
+ {},
+ undefined,
+ {},
+ {
+ type: 'MULTISELECT',
+ name: 'test_widget',
+ options: { values: [] }
+ }
+ )
const wrapper = mountComponent(widget, [])
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
@@ -215,7 +290,7 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('handles missing values option', () => {
- const widget = createMockWidget([])
+ const widget = createLocalMockWidget([])
const wrapper = mountComponent(widget, [])
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
@@ -226,7 +301,7 @@ describe('WidgetMultiSelect Value Binding', () => {
describe('Edge Cases', () => {
it('handles numeric values', async () => {
- const widget = createMockWidget([], {
+ const widget = createLocalMockWidget([], {
values: [1, 2, 3, 4, 5]
})
const wrapper = mountComponent(widget, [])
@@ -239,7 +314,7 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('handles mixed type values', async () => {
- const widget = createMockWidget([], {
+ const widget = createLocalMockWidget([], {
values: ['string', 123, true, null]
})
const wrapper = mountComponent(widget, [])
@@ -256,7 +331,7 @@ describe('WidgetMultiSelect Value Binding', () => {
{ id: 1, label: 'First' },
{ id: 2, label: 'Second' }
]
- const widget = createMockWidget([], {
+ const widget = createLocalMockWidget([], {
values: objectValues,
optionLabel: 'label',
optionValue: 'id'
@@ -271,7 +346,7 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('handles duplicate selections gracefully', async () => {
- const widget = createMockWidget([], {
+ const widget = createLocalMockWidget([], {
values: ['option1', 'option2']
})
const wrapper = mountComponent(widget, [])
@@ -290,7 +365,17 @@ describe('WidgetMultiSelect Value Binding', () => {
{ length: 1000 },
(_, i) => `option${i}`
)
- const widget = createMockWidget([], { values: largeOptionList })
+ const widget = createMockWidget(
+ [],
+ {},
+ undefined,
+ {},
+ {
+ type: 'MULTISELECT',
+ name: 'test_widget',
+ options: { values: largeOptionList }
+ }
+ )
const wrapper = mountComponent(widget, [])
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
@@ -298,7 +383,7 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('handles empty string values', async () => {
- const widget = createMockWidget([], {
+ const widget = createLocalMockWidget([], {
values: ['', 'not empty', ' ', 'normal']
})
const wrapper = mountComponent(widget, [])
@@ -313,7 +398,17 @@ describe('WidgetMultiSelect Value Binding', () => {
describe('Integration with Layout', () => {
it('renders within WidgetLayoutField', () => {
- const widget = createMockWidget([], { values: ['test'] })
+ const widget = createMockWidget(
+ [],
+ {},
+ undefined,
+ {},
+ {
+ type: 'MULTISELECT',
+ name: 'test_widget',
+ options: { values: ['test'] }
+ }
+ )
const wrapper = mountComponent(widget, [])
const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
@@ -322,7 +417,17 @@ describe('WidgetMultiSelect Value Binding', () => {
})
it('passes widget name to layout field', () => {
- const widget = createMockWidget([], { values: ['test'] })
+ const widget = createMockWidget(
+ [],
+ {},
+ undefined,
+ {},
+ {
+ type: 'MULTISELECT',
+ name: 'test_widget',
+ options: { values: ['test'] }
+ }
+ )
widget.name = 'custom_multiselect'
const wrapper = mountComponent(widget, [])
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue
index 167d45c2ca..ef01a89e0e 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue
@@ -23,6 +23,7 @@ import { computed } from 'vue'
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
+import { isMultiSelectInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
import {
PANEL_EXCLUDED_PROPS,
@@ -57,19 +58,20 @@ const MULTISELECT_EXCLUDED_PROPS = [
'overlayStyle'
] as const
+// Extract spec options directly
const combinedProps = computed(() => ({
...filterWidgetProps(props.widget.options, MULTISELECT_EXCLUDED_PROPS),
...transformCompatProps.value
}))
-// Extract multiselect options from widget options
+// Extract multiselect options from widget spec options
const multiSelectOptions = computed((): T[] => {
- const options = props.widget.options
-
- if (Array.isArray(options?.values)) {
- return options.values
+ const spec = props.widget.spec
+ if (!spec || !isMultiSelectInputSpec(spec)) {
+ return []
}
- return []
+ const values = spec.options?.values
+ return Array.isArray(values) ? (values as T[]) : []
})
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.test.ts
index 9eb13dd2ae..f37af54a60 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.test.ts
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.test.ts
@@ -3,23 +3,10 @@ import PrimeVue from 'primevue/config'
import { describe, expect, it, vi } from 'vitest'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
+import { createMockWidget } from '../testUtils'
import WidgetSelectButton from './WidgetSelectButton.vue'
-function createMockWidget(
- value: string = 'option1',
- options: SimplifiedWidget['options'] = {},
- callback?: (value: string) => void
-): SimplifiedWidget {
- return {
- name: 'test_selectbutton',
- type: 'string',
- value,
- options,
- callback
- }
-}
-
function mountComponent(
widget: SimplifiedWidget,
modelValue: string,
@@ -57,9 +44,20 @@ async function clickSelectButton(
describe('WidgetSelectButton Button Selection', () => {
describe('Basic Rendering', () => {
it('renders FormSelectButton component', () => {
- const widget = createMockWidget('option1', {
- values: ['option1', 'option2', 'option3']
- })
+ const widget = createMockWidget(
+ 'option1',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: ['option1', 'option2', 'option3'] }
+ }
+ )
const wrapper = mountComponent(widget, 'option1')
const formSelectButton = wrapper.findComponent({
@@ -70,7 +68,20 @@ describe('WidgetSelectButton Button Selection', () => {
it('renders buttons for each option', () => {
const options = ['first', 'second', 'third']
- const widget = createMockWidget('first', { values: options })
+ const widget = createMockWidget(
+ 'first',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'first')
const buttons = wrapper.findAll('button')
@@ -81,7 +92,20 @@ describe('WidgetSelectButton Button Selection', () => {
})
it('handles empty options array', () => {
- const widget = createMockWidget('', { values: [] })
+ const widget = createMockWidget(
+ '',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: [] }
+ }
+ )
const wrapper = mountComponent(widget, '')
const buttons = wrapper.findAll('button')
@@ -89,7 +113,7 @@ describe('WidgetSelectButton Button Selection', () => {
})
it('handles missing values option', () => {
- const widget = createMockWidget('')
+ const widget = createMockWidget('')
const wrapper = mountComponent(widget, '')
const buttons = wrapper.findAll('button')
@@ -100,7 +124,20 @@ describe('WidgetSelectButton Button Selection', () => {
describe('Selection State', () => {
it('highlights selected option', () => {
const options = ['apple', 'banana', 'cherry']
- const widget = createMockWidget('banana', { values: options })
+ const widget = createMockWidget(
+ 'banana',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'banana')
const buttons = wrapper.findAll('button')
@@ -119,7 +156,20 @@ describe('WidgetSelectButton Button Selection', () => {
it('handles no selection gracefully', () => {
const options = ['option1', 'option2']
- const widget = createMockWidget('nonexistent', { values: options })
+ const widget = createMockWidget(
+ 'nonexistent',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'nonexistent')
const buttons = wrapper.findAll('button')
@@ -135,7 +185,20 @@ describe('WidgetSelectButton Button Selection', () => {
context.skip('Classes not updating, needs diagnosis')
const options = ['first', 'second', 'third']
- const widget = createMockWidget('first', { values: options })
+ const widget = createMockWidget(
+ 'first',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'first')
// Initially 'first' is selected
@@ -159,7 +222,20 @@ describe('WidgetSelectButton Button Selection', () => {
describe('User Interactions', () => {
it('emits update:modelValue when button is clicked', async () => {
const options = ['first', 'second', 'third']
- const widget = createMockWidget('first', { values: options })
+ const widget = createMockWidget(
+ 'first',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'first')
await clickSelectButton(wrapper, 'second')
@@ -173,10 +249,19 @@ describe('WidgetSelectButton Button Selection', () => {
context.skip('Callback is not being called, needs diagnosis')
const mockCallback = vi.fn()
const options = ['option1', 'option2']
- const widget = createMockWidget(
+ const widget = createMockWidget(
'option1',
- { values: options },
- mockCallback
+ {},
+ mockCallback,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
)
const wrapper = mountComponent(widget, 'option1')
@@ -187,7 +272,20 @@ describe('WidgetSelectButton Button Selection', () => {
it('handles missing callback gracefully', async () => {
const options = ['option1', 'option2']
- const widget = createMockWidget('option1', { values: options }, undefined)
+ const widget = createMockWidget(
+ 'option1',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'option1')
await clickSelectButton(wrapper, 'option2')
@@ -200,7 +298,20 @@ describe('WidgetSelectButton Button Selection', () => {
it('allows clicking same option again', async () => {
const options = ['option1', 'option2']
- const widget = createMockWidget('option1', { values: options })
+ const widget = createMockWidget(
+ 'option1',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'option1')
await clickSelectButton(wrapper, 'option1')
@@ -214,7 +325,20 @@ describe('WidgetSelectButton Button Selection', () => {
describe('Option Types', () => {
it('handles string options', () => {
const options = ['apple', 'banana', 'cherry']
- const widget = createMockWidget('banana', { values: options })
+ const widget = createMockWidget(
+ 'banana',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'banana')
const buttons = wrapper.findAll('button')
@@ -225,7 +349,20 @@ describe('WidgetSelectButton Button Selection', () => {
it('handles number options', () => {
const options = [1, 2, 3]
- const widget = createMockWidget('2', { values: options })
+ const widget = createMockWidget(
+ '2',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, '2')
const buttons = wrapper.findAll('button')
@@ -245,7 +382,20 @@ describe('WidgetSelectButton Button Selection', () => {
{ label: 'Second Option', value: 'second' },
{ label: 'Third Option', value: 'third' }
]
- const widget = createMockWidget('second', { values: options })
+ const widget = createMockWidget(
+ 'second',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'second')
const buttons = wrapper.findAll('button')
@@ -264,7 +414,20 @@ describe('WidgetSelectButton Button Selection', () => {
{ label: 'First', value: 'first_val' },
{ label: 'Second', value: 'second_val' }
]
- const widget = createMockWidget('first_val', { values: options })
+ const widget = createMockWidget(
+ 'first_val',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'first_val')
await clickSelectButton(wrapper, 'Second')
@@ -278,7 +441,20 @@ describe('WidgetSelectButton Button Selection', () => {
describe('Edge Cases', () => {
it('handles options with special characters', () => {
const options = ['@#$%^&*()', '{}[]|\\:";\'<>?,./']
- const widget = createMockWidget(options[0], { values: options })
+ const widget = createMockWidget(
+ options[0],
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, options[0])
const buttons = wrapper.findAll('button')
@@ -288,7 +464,20 @@ describe('WidgetSelectButton Button Selection', () => {
it('handles empty string options', () => {
const options = ['', 'not empty', ' ', 'normal']
- const widget = createMockWidget('', { values: options })
+ const widget = createMockWidget(
+ '',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, '')
const buttons = wrapper.findAll('button')
@@ -305,7 +494,20 @@ describe('WidgetSelectButton Button Selection', () => {
undefined,
'another'
]
- const widget = createMockWidget('valid', { values: options })
+ const widget = createMockWidget(
+ 'valid',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'valid')
const buttons = wrapper.findAll('button')
@@ -319,7 +521,20 @@ describe('WidgetSelectButton Button Selection', () => {
const longText =
'This is a very long option text that might cause layout issues if not handled properly'
const options = ['short', longText, 'normal']
- const widget = createMockWidget('short', { values: options })
+ const widget = createMockWidget(
+ 'short',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'short')
const buttons = wrapper.findAll('button')
@@ -328,7 +543,20 @@ describe('WidgetSelectButton Button Selection', () => {
it('handles large number of options', () => {
const options = Array.from({ length: 20 }, (_, i) => `option${i + 1}`)
- const widget = createMockWidget('option5', { values: options })
+ const widget = createMockWidget(
+ 'option5',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'option5')
const buttons = wrapper.findAll('button')
@@ -340,7 +568,20 @@ describe('WidgetSelectButton Button Selection', () => {
it('handles duplicate options', () => {
const options = ['duplicate', 'unique', 'duplicate', 'unique']
- const widget = createMockWidget('duplicate', { values: options })
+ const widget = createMockWidget(
+ 'duplicate',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'duplicate')
const buttons = wrapper.findAll('button')
@@ -358,7 +599,20 @@ describe('WidgetSelectButton Button Selection', () => {
describe('Styling and Layout', () => {
it('applies proper button styling', () => {
const options = ['option1', 'option2']
- const widget = createMockWidget('option1', { values: options })
+ const widget = createMockWidget(
+ 'option1',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'option1')
const buttons = wrapper.findAll('button')
@@ -374,7 +628,20 @@ describe('WidgetSelectButton Button Selection', () => {
it('applies hover effects for non-selected options', () => {
const options = ['option1', 'option2']
- const widget = createMockWidget('option1', { values: options })
+ const widget = createMockWidget(
+ 'option1',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: options }
+ }
+ )
const wrapper = mountComponent(widget, 'option1', false)
const buttons = wrapper.findAll('button')
@@ -389,7 +656,20 @@ describe('WidgetSelectButton Button Selection', () => {
describe('Integration with Layout', () => {
it('renders within WidgetLayoutField', () => {
- const widget = createMockWidget('test', { values: ['test'] })
+ const widget = createMockWidget(
+ 'test',
+ {},
+ undefined,
+ {
+ name: 'test_selectbutton',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'test_selectbutton',
+ options: { values: ['test'] }
+ }
+ )
const wrapper = mountComponent(widget, 'test')
const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
@@ -398,8 +678,20 @@ describe('WidgetSelectButton Button Selection', () => {
})
it('passes widget name to layout field', () => {
- const widget = createMockWidget('test', { values: ['test'] })
- widget.name = 'custom_select_button'
+ const widget = createMockWidget(
+ 'test',
+ {},
+ undefined,
+ {
+ name: 'custom_select_button',
+ type: 'string'
+ },
+ {
+ type: 'SELECTBUTTON',
+ name: 'custom_select_button',
+ options: { values: ['test'] }
+ }
+ )
const wrapper = mountComponent(widget, 'test')
const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue
index 9a5663957c..9e3d08beb3 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue
@@ -2,7 +2,7 @@
@@ -10,7 +10,10 @@
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue
index fae52b5302..b52fe066a8 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue
@@ -20,6 +20,7 @@ import { computed } from 'vue'
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
+import { isTreeSelectInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
PANEL_EXCLUDED_PROPS,
@@ -64,8 +65,28 @@ const TREE_SELECT_EXCLUDED_PROPS = [
'inputStyle'
] as const
-const combinedProps = computed(() => ({
- ...filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS),
- ...transformCompatProps.value
-}))
+const combinedProps = computed(() => {
+ const spec = props.widget.spec
+ if (!spec || !isTreeSelectInputSpec(spec)) {
+ return {
+ ...filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS),
+ ...transformCompatProps.value
+ }
+ }
+
+ const specOptions = spec.options || {}
+ return {
+ // Include runtime props like disabled, but filter out panel-related ones
+ ...filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS),
+ // PrimeVue TreeSelect expects 'options' to be an array of tree nodes
+ options: (specOptions.values as TreeNode[]) || [],
+ // Convert 'multiple' to PrimeVue's 'selectionMode'
+ selectionMode: specOptions.multiple
+ ? ('multiple' as const)
+ : ('single' as const),
+ // Pass through other props like placeholder
+ placeholder: specOptions.placeholder,
+ ...transformCompatProps.value
+ }
+})
diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts b/src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts
index 9ecd8a5796..a88686743a 100644
--- a/src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts
+++ b/src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts
@@ -24,7 +24,7 @@ import type { BaseDOMWidget } from '@/scripts/domWidget'
import { addValueControlWidgets } from '@/scripts/widgets'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
import { useAssetsStore } from '@/stores/assetsStore'
-import { getMediaTypeFromFilename } from '@/utils/formatUtil'
+import { getMediaTypeFromFilename } from '@comfyorg/shared-frontend-utils/formatUtil'
import { useRemoteWidget } from './useRemoteWidget'
diff --git a/src/renderer/extensions/vueNodes/widgets/testUtils.ts b/src/renderer/extensions/vueNodes/widgets/testUtils.ts
index c8d1125281..22f74a4d6c 100644
--- a/src/renderer/extensions/vueNodes/widgets/testUtils.ts
+++ b/src/renderer/extensions/vueNodes/widgets/testUtils.ts
@@ -1,16 +1,18 @@
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
+import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
/**
* Creates a mock SimplifiedWidget for testing Vue Node widgets.
* This utility function is shared across widget component tests to ensure consistency.
*/
-export function createMockWidget(
+export function createMockWidget(
value: T = null as T,
options: Record = {},
callback?: (value: T) => void,
- overrides: Partial> = {}
+ overrides: Partial> = {},
+ spec?: Partial
): SimplifiedWidget {
- return {
+ const widget: SimplifiedWidget = {
name: 'test_widget',
type: 'default',
value,
@@ -18,6 +20,13 @@ export function createMockWidget(
callback,
...overrides
}
+
+ // Only add spec if provided
+ if (spec) {
+ widget.spec = spec as InputSpec
+ }
+
+ return widget
}
/**
diff --git a/src/schemas/nodeDef/nodeDefSchemaV2.ts b/src/schemas/nodeDef/nodeDefSchemaV2.ts
index 09983d115e..7cd9c26756 100644
--- a/src/schemas/nodeDef/nodeDefSchemaV2.ts
+++ b/src/schemas/nodeDef/nodeDefSchemaV2.ts
@@ -45,7 +45,8 @@ const zColorInputSpec = zBaseInputOptions.extend({
isOptional: z.boolean().optional(),
options: z
.object({
- default: z.string().optional()
+ default: z.string().optional(),
+ format: z.enum(['hex', 'rgb', 'hsl', 'hsb']).optional()
})
.optional()
})
@@ -54,7 +55,13 @@ const zFileUploadInputSpec = zBaseInputOptions.extend({
type: z.literal('FILEUPLOAD'),
name: z.string(),
isOptional: z.boolean().optional(),
- options: z.record(z.unknown()).optional()
+ options: z
+ .object({
+ accept: z.string().optional(),
+ extensions: z.array(z.string()).optional(),
+ tooltip: z.string().optional()
+ })
+ .optional()
})
const zImageInputSpec = zBaseInputOptions.extend({
@@ -89,7 +96,8 @@ const zTreeSelectInputSpec = zBaseInputOptions.extend({
options: z
.object({
multiple: z.boolean().optional(),
- values: z.array(z.unknown()).optional()
+ values: z.array(z.unknown()).optional(),
+ placeholder: z.string().optional()
})
.optional()
})
@@ -123,7 +131,9 @@ const zGalleriaInputSpec = zBaseInputOptions.extend({
isOptional: z.boolean().optional(),
options: z
.object({
- images: z.array(z.string()).optional()
+ images: z.array(z.string()).optional(),
+ showThumbnails: z.boolean().optional(),
+ showItemNavigators: z.boolean().optional()
})
.optional()
})
@@ -214,6 +224,7 @@ type StringInputSpec = z.infer
export type ComboInputSpec = z.infer
export type ColorInputSpec = z.infer
export type FileUploadInputSpec = z.infer
+type ImageInputSpec = z.infer
export type ImageCompareInputSpec = z.infer
export type TreeSelectInputSpec = z.infer
export type MultiSelectInputSpec = z.infer
@@ -262,3 +273,46 @@ export const isChartInputSpec = (
): inputSpec is ChartInputSpec => {
return inputSpec.type === 'CHART'
}
+
+export const isTreeSelectInputSpec = (
+ inputSpec: InputSpec
+): inputSpec is TreeSelectInputSpec => {
+ return inputSpec.type === 'TREESELECT'
+}
+
+export const isSelectButtonInputSpec = (
+ inputSpec: InputSpec
+): inputSpec is SelectButtonInputSpec => {
+ return inputSpec.type === 'SELECTBUTTON'
+}
+
+export const isMultiSelectInputSpec = (
+ inputSpec: InputSpec
+): inputSpec is MultiSelectInputSpec => {
+ return inputSpec.type === 'MULTISELECT'
+}
+
+export const isGalleriaInputSpec = (
+ inputSpec: InputSpec
+): inputSpec is GalleriaInputSpec => {
+ return inputSpec.type === 'GALLERIA'
+}
+
+export const isColorInputSpec = (
+ inputSpec: InputSpec
+): inputSpec is ColorInputSpec => {
+ return inputSpec.type === 'COLOR'
+}
+
+export const isFileUploadInputSpec = (
+ inputSpec: InputSpec
+): inputSpec is FileUploadInputSpec => {
+ return inputSpec.type === 'FILEUPLOAD'
+}
+
+// @ts-expect-error - will be used in future IMAGE widget implementation
+const isImageInputSpec = (
+ inputSpec: InputSpec
+): inputSpec is ImageInputSpec => {
+ return inputSpec.type === 'IMAGE'
+}
diff --git a/src/scripts/domWidget.ts b/src/scripts/domWidget.ts
index a550a41457..bca2885271 100644
--- a/src/scripts/domWidget.ts
+++ b/src/scripts/domWidget.ts
@@ -13,7 +13,7 @@ import type {
} from '@/lib/litegraph/src/types/widgets'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
-import { generateUUID } from '@/utils/formatUtil'
+import { generateUUID } from '@comfyorg/shared-frontend-utils/formatUtil'
export interface BaseDOMWidget
extends IBaseWidget> {
diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts
index 35562533b2..eacbc5f38d 100644
--- a/src/services/litegraphService.ts
+++ b/src/services/litegraphService.ts
@@ -49,7 +49,7 @@ import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
import { useWidgetStore } from '@/stores/widgetStore'
-import { normalizeI18nKey } from '@/utils/formatUtil'
+import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
import {
isImageNode,
isVideoNode,
diff --git a/src/services/providers/algoliaSearchProvider.ts b/src/services/providers/algoliaSearchProvider.ts
index 7edf985809..60a14641a1 100644
--- a/src/services/providers/algoliaSearchProvider.ts
+++ b/src/services/providers/algoliaSearchProvider.ts
@@ -22,7 +22,7 @@ import type {
SearchPacksResult,
SortableField
} from '@/types/searchServiceTypes'
-import { paramsToCacheKey } from '@/utils/formatUtil'
+import { paramsToCacheKey } from '@comfyorg/shared-frontend-utils/formatUtil'
import { SortableAlgoliaField } from '@/workbench/extensions/manager/types/comfyManagerTypes'
type RegistryNodePack = components['schemas']['Node']
diff --git a/src/stores/imagePreviewStore.ts b/src/stores/imagePreviewStore.ts
index 300c532c97..aea689e95c 100644
--- a/src/stores/imagePreviewStore.ts
+++ b/src/stores/imagePreviewStore.ts
@@ -13,7 +13,7 @@ import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useExecutionStore } from '@/stores/executionStore'
import type { NodeLocatorId } from '@/types/nodeIdentification'
-import { parseFilePath } from '@/utils/formatUtil'
+import { parseFilePath } from '@comfyorg/shared-frontend-utils/formatUtil'
import { isVideoNode } from '@/utils/litegraphUtil'
const PREVIEW_REVOKE_DELAY_MS = 400
diff --git a/src/stores/queueStore.ts b/src/stores/queueStore.ts
index 34a66d5c1b..397aee597a 100644
--- a/src/stores/queueStore.ts
+++ b/src/stores/queueStore.ts
@@ -23,7 +23,7 @@ import { api } from '@/scripts/api'
import type { ComfyApp } from '@/scripts/app'
import { useExtensionService } from '@/services/extensionService'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
-import { getMediaTypeFromFilename } from '@/utils/formatUtil'
+import { getMediaTypeFromFilename } from '@comfyorg/shared-frontend-utils/formatUtil'
// Task type used in the API.
type APITaskType = 'queue' | 'history'
diff --git a/src/stores/userFileStore.ts b/src/stores/userFileStore.ts
index f2742f3e3a..4eef7689a1 100644
--- a/src/stores/userFileStore.ts
+++ b/src/stores/userFileStore.ts
@@ -4,7 +4,7 @@ import { computed, ref } from 'vue'
import type { UserDataFullInfo } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import type { TreeExplorerNode } from '@/types/treeExplorerTypes'
-import { getPathDetails } from '@/utils/formatUtil'
+import { getPathDetails } from '@comfyorg/shared-frontend-utils/formatUtil'
import { syncEntities } from '@/utils/syncUtil'
import { buildTree } from '@/utils/treeUtil'
diff --git a/src/utils/electronMirrorCheck.ts b/src/utils/electronMirrorCheck.ts
index 8242fb6fa4..2abe3301c0 100644
--- a/src/utils/electronMirrorCheck.ts
+++ b/src/utils/electronMirrorCheck.ts
@@ -1,5 +1,5 @@
import { electronAPI } from '@/utils/envUtil'
-import { isValidUrl } from '@/utils/formatUtil'
+import { isValidUrl } from '@comfyorg/shared-frontend-utils/formatUtil'
/**
* Check if a mirror is reachable from the electron App.
diff --git a/src/utils/searchAndReplace.ts b/src/utils/searchAndReplace.ts
index 1db9c8ee84..3f12f3187f 100644
--- a/src/utils/searchAndReplace.ts
+++ b/src/utils/searchAndReplace.ts
@@ -1,5 +1,5 @@
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
-import { formatDate } from '@/utils/formatUtil'
+import { formatDate } from '@comfyorg/shared-frontend-utils/formatUtil'
import { collectAllNodes } from '@/utils/graphTraversalUtil'
export function applyTextReplacements(
diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/tabs/DescriptionTabPanel.vue b/src/workbench/extensions/manager/components/manager/infoPanel/tabs/DescriptionTabPanel.vue
index e6b203cd8b..3ca674e649 100644
--- a/src/workbench/extensions/manager/components/manager/infoPanel/tabs/DescriptionTabPanel.vue
+++ b/src/workbench/extensions/manager/components/manager/infoPanel/tabs/DescriptionTabPanel.vue
@@ -23,11 +23,11 @@