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 @@