diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index 3a6b6154c..352eef4e3 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -87,6 +87,34 @@ export interface GraphNodeManager { cleanup(): void } +function normalizeWidgetValue(value: unknown): WidgetValue { + if (value === null || value === undefined || value === void 0) { + return undefined + } + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value + } + if (typeof value === 'object') { + // Check if it's a File array + if ( + Array.isArray(value) && + value.length > 0 && + value.every((item): item is File => item instanceof File) + ) { + return value + } + // Otherwise it's a generic object + return value + } + // If none of the above, return undefined + console.warn(`Invalid widget value type: ${typeof value}`, value) + return undefined +} + function getControlWidget(widget: IBaseWidget): (() => Ref)|undefined { const cagWidget = widget.linkedWidgets?.find( (w) => w.name == 'control_after_generate' @@ -127,7 +155,7 @@ export function safeWidgetMapper( }) widget.callback = useChainCallback(widget.callback, () => { if (valueRef.value !== widget.value) - valueRef.value = validateWidgetValue(widget.value) ?? undefined + valueRef.value = normalizeWidgetValue(widget.value) ?? undefined }) widget.valueRef = () => valueRef } diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.test.ts index eb1219aa8..ba2302a04 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.test.ts @@ -199,11 +199,7 @@ describe('WidgetButton Interactions', () => { const widget = createMockWidget({}, mockCallback) const wrapper = mountComponent(widget) - // Simulate rapid clicks - const clickPromises = Array.from({ length: 16 }, () => - clickButton(wrapper) - ) - await Promise.all(clickPromises) + for (let i = 0; i < 16; i++) await clickButton(wrapper) expect(mockCallback).toHaveBeenCalledTimes(16) }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue index a55da958c..dfc217fa6 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue @@ -16,7 +16,7 @@ diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts index a6c343d5a..f730efd35 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts @@ -15,7 +15,7 @@ function createMockWidget( callback?: (value: number) => void ): SimplifiedWidget { const valueRef = ref(value) - if (callback) watch(valueRef, callback) + if (callback) watch(valueRef, (v) => callback(v)) return { name: 'test_input_number', type, @@ -52,15 +52,14 @@ describe('WidgetInputNumberInput Value Binding', () => { }) it('emits update:modelValue when value changes', async () => { - const widget = createMockWidget(10, 'int') + const callback = vi.fn() + const widget = createMockWidget(10, 'int', {}, callback) const wrapper = mountComponent(widget, 10) const inputNumber = wrapper.findComponent(InputNumber) - await inputNumber.vm.$emit('update:modelValue', 20) + await inputNumber.setValue(20) - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain(20) + expect(callback).toHaveBeenCalledExactlyOnceWith(20) }) it('handles negative values', () => { diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.test.ts index 1be2ceb68..537c72cda 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.test.ts @@ -17,7 +17,7 @@ describe('WidgetInputText Value Binding', () => { callback?: (value: string) => void ): SimplifiedWidget => { const valueRef = ref(value) - if (callback) watch(valueRef, callback) + if (callback) watch(valueRef, (v) => callback(v)) return { name: 'test_input', type: 'string', @@ -58,86 +58,26 @@ describe('WidgetInputText Value Binding', () => { return input } - describe('Vue Event Emission', () => { - it('emits Vue event when input value changes on blur', async () => { - const widget = createMockWidget('hello') - const wrapper = mountComponent(widget, 'hello') - - await setInputValueAndTrigger(wrapper, 'world', 'blur') - - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('world') - }) - - it('emits Vue event when enter key is pressed', async () => { - const widget = createMockWidget('initial') - const wrapper = mountComponent(widget, 'initial') - - await setInputValueAndTrigger(wrapper, 'new value', 'keydown.enter') - - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('new value') - }) - + describe('Widget Value Callbacks', () => { it('handles empty string values', async () => { - const widget = createMockWidget('something') + const callback = vi.fn() + const widget = createMockWidget('something', {}, callback) const wrapper = mountComponent(widget, 'something') await setInputValueAndTrigger(wrapper, '') - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('') + expect(callback).toHaveBeenCalledExactlyOnceWith('') }) it('handles special characters correctly', async () => { - const widget = createMockWidget('normal') + const callback = vi.fn() + const widget = createMockWidget('normal', {}, callback) const wrapper = mountComponent(widget, 'normal') const specialText = 'special @#$%^&*()[]{}|\\:";\'<>?,./' await setInputValueAndTrigger(wrapper, specialText) - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain(specialText) - }) - - it('handles missing callback gracefully', async () => { - const widget = createMockWidget('test', {}, undefined) - const wrapper = mountComponent(widget, 'test') - - await setInputValueAndTrigger(wrapper, 'new value') - - // Should still emit Vue event - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('new value') - }) - }) - - describe('User Interactions', () => { - it('emits update:modelValue on blur', async () => { - const widget = createMockWidget('original') - const wrapper = mountComponent(widget, 'original') - - await setInputValueAndTrigger(wrapper, 'updated') - - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('updated') - }) - - it('emits update:modelValue on enter key', async () => { - const widget = createMockWidget('start') - const wrapper = mountComponent(widget, 'start') - - await setInputValueAndTrigger(wrapper, 'finish', 'keydown.enter') - - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('finish') + expect(callback).toHaveBeenCalledExactlyOnceWith(specialText) }) }) @@ -158,27 +98,25 @@ describe('WidgetInputText Value Binding', () => { describe('Edge Cases', () => { it('handles very long strings', async () => { - const widget = createMockWidget('short') + const callback = vi.fn() + const widget = createMockWidget('short', {}, callback) const wrapper = mountComponent(widget, 'short') const longString = 'a'.repeat(10000) await setInputValueAndTrigger(wrapper, longString) - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain(longString) + expect(callback).toHaveBeenCalledExactlyOnceWith(longString) }) it('handles unicode characters', async () => { - const widget = createMockWidget('ascii') + const callback = vi.fn() + const widget = createMockWidget('ascii', {}, callback) const wrapper = mountComponent(widget, 'ascii') const unicodeText = '🎨 Unicode: αβγ 中文 العربية 🚀' await setInputValueAndTrigger(wrapper, unicodeText) - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain(unicodeText) + expect(callback).toHaveBeenCalledExactlyOnceWith(unicodeText) }) }) }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts index f3e6c9c53..1bac6b678 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts @@ -30,7 +30,7 @@ describe('WidgetMarkdown Dual Mode Display', () => { callback?: (value: string) => void ): SimplifiedWidget => { const valueRef = ref(value) - if (callback) watch(valueRef, callback) + if (callback) watch(valueRef, (v) => callback(v)) return { name: 'test_markdown', type: 'string', @@ -213,7 +213,8 @@ describe('WidgetMarkdown Dual Mode Display', () => { describe('Value Updates', () => { it('emits update:modelValue when textarea content changes', async () => { - const widget = createMockWidget('# Original') + const callback = vi.fn() + const widget = createMockWidget('# Original', {}, callback) const wrapper = mountComponent(widget, '# Original') await clickToEdit(wrapper) @@ -222,9 +223,7 @@ describe('WidgetMarkdown Dual Mode Display', () => { await textarea.setValue('# Updated Content') await textarea.trigger('input') - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![emitted!.length - 1]).toEqual(['# Updated Content']) + expect(callback).toHaveBeenLastCalledWith('# Updated Content') }) it('renders updated HTML after value change and blur', async () => { @@ -242,38 +241,6 @@ describe('WidgetMarkdown Dual Mode Display', () => { expect(displayDiv.html()).toContain('

New Heading

') expect(displayDiv.html()).toContain('bold') }) - - it('emits update:modelValue for callback handling at parent level', async () => { - const widget = createMockWidget('# Test', {}) - const wrapper = mountComponent(widget, '# Test') - - await clickToEdit(wrapper) - - const textarea = wrapper.find('textarea') - await textarea.setValue('# Changed') - await textarea.trigger('input') - - // The widget should emit the change for parent (NodeWidgets) to handle callbacks - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![emitted!.length - 1]).toEqual(['# Changed']) - }) - - it('handles missing callback gracefully', async () => { - const widget = createMockWidget('# Test', {}, undefined) - const wrapper = mountComponent(widget, '# Test') - - await clickToEdit(wrapper) - - const textarea = wrapper.find('textarea') - await textarea.setValue('# Changed') - - // Should not throw error and should still emit Vue event - await expect(textarea.trigger('input')).resolves.not.toThrow() - - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - }) }) describe('Complex Markdown Rendering', () => { @@ -340,8 +307,9 @@ Another line with more content.` }) it('handles unicode characters', async () => { + const callback = vi.fn() const unicode = '# Unicode: 🎨 αβγ 中文 العربية 🚀' - const widget = createMockWidget(unicode) + const widget = createMockWidget(unicode, {}, callback) const wrapper = mountComponent(widget, unicode) await clickToEdit(wrapper) @@ -351,9 +319,7 @@ Another line with more content.` await textarea.setValue(unicode + ' more unicode') await textarea.trigger('input') - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![emitted!.length - 1]).toEqual([unicode + ' more unicode']) + expect(callback).toHaveBeenLastCalledWith(unicode + ' more unicode') }) it('handles rapid edit mode toggling', async () => { diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts index f89932cd6..9ff644fa3 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts @@ -14,7 +14,7 @@ function createMockWidget( callback?: (value: string) => void ): SimplifiedWidget { const valueRef = ref(value) - if (callback) watch(valueRef, callback) + if (callback) watch(valueRef, (v) => callback(v)) return { name: 'test_textarea', type: 'string', @@ -60,98 +60,47 @@ async function setTextareaValueAndTrigger( } describe('WidgetTextarea Value Binding', () => { - describe('Vue Event Emission', () => { - it('emits Vue event when textarea value changes on blur', async () => { - const widget = createMockWidget('hello') - const wrapper = mountComponent(widget, 'hello') - - await setTextareaValueAndTrigger(wrapper, 'world', 'blur') - - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted?.[0]).toContain('world') - }) - + describe('Widget Value Callbacks', () => { it('emits Vue event when textarea value changes on input', async () => { - const widget = createMockWidget('initial') + const callback = vi.fn() + const widget = createMockWidget('initial', {}, callback) const wrapper = mountComponent(widget, 'initial') await setTextareaValueAndTrigger(wrapper, 'new content', 'input') - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted?.[0]).toContain('new content') + expect(callback).toHaveBeenCalledExactlyOnceWith('new content') }) it('handles empty string values', async () => { - const widget = createMockWidget('something') + const callback = vi.fn() + const widget = createMockWidget('something', {}, callback) const wrapper = mountComponent(widget, 'something') await setTextareaValueAndTrigger(wrapper, '') - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted?.[0]).toContain('') + expect(callback).toHaveBeenCalledExactlyOnceWith('') }) it('handles multiline text correctly', async () => { - const widget = createMockWidget('single line') + const callback = vi.fn() + const widget = createMockWidget('single line', {}, callback) const wrapper = mountComponent(widget, 'single line') const multilineText = 'Line 1\nLine 2\nLine 3' await setTextareaValueAndTrigger(wrapper, multilineText) - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted?.[0]).toContain(multilineText) + expect(callback).toHaveBeenCalledExactlyOnceWith(multilineText) }) it('handles special characters correctly', async () => { - const widget = createMockWidget('normal') + const callback = vi.fn() + const widget = createMockWidget('normal', {}, callback) const wrapper = mountComponent(widget, 'normal') const specialText = 'special @#$%^&*()[]{}|\\:";\'<>?,./' await setTextareaValueAndTrigger(wrapper, specialText) - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted?.[0]).toContain(specialText) - }) - - it('handles missing callback gracefully', async () => { - const widget = createMockWidget('test', {}, undefined) - const wrapper = mountComponent(widget, 'test') - - await setTextareaValueAndTrigger(wrapper, 'new value') - - // Should still emit Vue event - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted?.[0]).toContain('new value') - }) - }) - - describe('User Interactions', () => { - it('emits update:modelValue on blur', async () => { - const widget = createMockWidget('original') - const wrapper = mountComponent(widget, 'original') - - await setTextareaValueAndTrigger(wrapper, 'updated') - - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted?.[0]).toContain('updated') - }) - - it('emits update:modelValue on input', async () => { - const widget = createMockWidget('start') - const wrapper = mountComponent(widget, 'start') - - await setTextareaValueAndTrigger(wrapper, 'finish', 'input') - - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted?.[0]).toContain('finish') + expect(callback).toHaveBeenCalledExactlyOnceWith(specialText) }) }) @@ -201,39 +150,36 @@ describe('WidgetTextarea Value Binding', () => { describe('Edge Cases', () => { it('handles very long text', async () => { - const widget = createMockWidget('short') + const callback = vi.fn() + const widget = createMockWidget('short', {}, callback) const wrapper = mountComponent(widget, 'short') const longText = 'a'.repeat(10000) await setTextareaValueAndTrigger(wrapper, longText) - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted?.[0]).toContain(longText) + expect(callback).toHaveBeenCalledExactlyOnceWith(longText) }) it('handles unicode characters', async () => { - const widget = createMockWidget('ascii') + const callback = vi.fn() + const widget = createMockWidget('ascii', {}, callback) const wrapper = mountComponent(widget, 'ascii') const unicodeText = '🎨 Unicode: αβγ 中文 العربية 🚀' await setTextareaValueAndTrigger(wrapper, unicodeText) - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted?.[0]).toContain(unicodeText) + expect(callback).toHaveBeenCalledExactlyOnceWith(unicodeText) }) it('handles text with tabs and spaces', async () => { - const widget = createMockWidget('normal') + const callback = vi.fn() + const widget = createMockWidget('normal', {}, callback) const wrapper = mountComponent(widget, 'normal') const formattedText = '\tIndented line\n Spaced line\n\t\tDouble indent' await setTextareaValueAndTrigger(wrapper, formattedText) - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted?.[0]).toContain(formattedText) + expect(callback).toHaveBeenCalledExactlyOnceWith(formattedText) }) }) }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts index bb702f312..cc5825b1c 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts @@ -16,7 +16,7 @@ describe('WidgetToggleSwitch Value Binding', () => { callback?: (value: boolean) => void ): SimplifiedWidget => { const valueRef = ref(value) - if (callback) watch(valueRef, callback) + if (callback) watch(valueRef, (v) => callback(v)) return { name: 'test_toggle', type: 'boolean', @@ -43,48 +43,6 @@ describe('WidgetToggleSwitch Value Binding', () => { }) } - describe('Vue Event Emission', () => { - it('emits Vue event when toggled from false to true', async () => { - const widget = createMockWidget(false) - const wrapper = mountComponent(widget, false) - - const toggle = wrapper.findComponent({ name: 'ToggleSwitch' }) - await toggle.setValue(true) - - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain(true) - }) - - it('emits Vue event when toggled from true to false', async () => { - const widget = createMockWidget(true) - const wrapper = mountComponent(widget, true) - - const toggle = wrapper.findComponent({ name: 'ToggleSwitch' }) - await toggle.setValue(false) - - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain(false) - }) - - it('handles value changes gracefully', async () => { - const widget = createMockWidget(false) - const wrapper = mountComponent(widget, false) - - // Should not throw when changing values - const toggle = wrapper.findComponent({ name: 'ToggleSwitch' }) - await toggle.setValue(true) - await toggle.setValue(false) - - // Should emit events for all changes - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toHaveLength(2) - expect(emitted![0]).toContain(true) - expect(emitted![1]).toContain(false) - }) - }) - describe('Component Rendering', () => { it('renders toggle switch component', () => { const widget = createMockWidget(false) @@ -113,27 +71,9 @@ describe('WidgetToggleSwitch Value Binding', () => { }) describe('Multiple Value Changes', () => { - it('handles rapid toggling correctly', async () => { - const widget = createMockWidget(false) - const wrapper = mountComponent(widget, false) - - const toggle = wrapper.findComponent({ name: 'ToggleSwitch' }) - - // Rapid toggle sequence - await toggle.setValue(true) - await toggle.setValue(false) - await toggle.setValue(true) - - // Should have emitted 3 Vue events with correct values - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toHaveLength(3) - expect(emitted![0]).toContain(true) - expect(emitted![1]).toContain(false) - expect(emitted![2]).toContain(true) - }) - it('maintains state consistency during multiple changes', async () => { - const widget = createMockWidget(false) + const callback = vi.fn() + const widget = createMockWidget(false, {}, callback) const wrapper = mountComponent(widget, false) const toggle = wrapper.findComponent({ name: 'ToggleSwitch' }) @@ -144,13 +84,12 @@ describe('WidgetToggleSwitch Value Binding', () => { await toggle.setValue(true) await toggle.setValue(false) - const emitted = wrapper.emitted('update:modelValue') - expect(emitted).toHaveLength(4) + expect(callback).toHaveBeenCalledTimes(4) // Verify alternating pattern - expect(emitted![0]).toContain(true) - expect(emitted![1]).toContain(false) - expect(emitted![2]).toContain(true) - expect(emitted![3]).toContain(false) + expect(callback).toHaveBeenNthCalledWith(1, true) + expect(callback).toHaveBeenNthCalledWith(2, false) + expect(callback).toHaveBeenNthCalledWith(3, true) + expect(callback).toHaveBeenNthCalledWith(4, false) }) }) }) diff --git a/tests-ui/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts b/tests-ui/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts index 7358604d7..ee30f92b0 100644 --- a/tests-ui/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts +++ b/tests-ui/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts @@ -54,7 +54,7 @@ describe('WidgetSelect Value Binding', () => { spec?: ComboInputSpec ): SimplifiedWidget => { const valueRef = ref(value) - if (callback) watch(valueRef, callback) + if (callback) watch(valueRef, (v) => callback(v)) return { name: 'test_select', type: 'combo', @@ -85,67 +85,57 @@ describe('WidgetSelect Value Binding', () => { }) } - const setSelectValueAndEmit = async ( + const setSelectValue = async ( wrapper: ReturnType, value: string ) => { const select = wrapper.findComponent({ name: 'Select' }) await select.setValue(value) - return wrapper.emitted('update:modelValue') } - describe('Vue Event Emission', () => { - it('emits Vue event when selection changes', async () => { - const widget = createMockWidget('option1') + describe('Widget Value Callbacks', () => { + it('triggers callback when selection changes', async () => { + const callback = vi.fn() + const widget = createMockWidget('option1', {}, callback) const wrapper = mountComponent(widget, 'option1') - const emitted = await setSelectValueAndEmit(wrapper, 'option2') + await setSelectValue(wrapper, 'option2') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('option2') + expect(callback).toHaveBeenCalledExactlyOnceWith('option2') }) - it('emits string value for different options', async () => { - const widget = createMockWidget('option1') + it('handles string value for different options', async () => { + const callback = vi.fn() + const widget = createMockWidget('option1', {}, callback) const wrapper = mountComponent(widget, 'option1') - const emitted = await setSelectValueAndEmit(wrapper, 'option3') - - expect(emitted).toBeDefined() - // Should emit the string value - expect(emitted![0]).toContain('option3') + await setSelectValue(wrapper, 'option3') + expect(callback).toHaveBeenCalledExactlyOnceWith('option3') }) it('handles custom option values', async () => { const customOptions = ['custom_a', 'custom_b', 'custom_c'] - const widget = createMockWidget('custom_a', { values: customOptions }) + const callback = vi.fn() + const widget = createMockWidget( + 'custom_a', + { values: customOptions }, + callback + ) const wrapper = mountComponent(widget, 'custom_a') - const emitted = await setSelectValueAndEmit(wrapper, 'custom_b') + await setSelectValue(wrapper, 'custom_b') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('custom_b') - }) - - it('handles missing callback gracefully', async () => { - const widget = createMockWidget('option1', {}, undefined) - const wrapper = mountComponent(widget, 'option1') - - const emitted = await setSelectValueAndEmit(wrapper, 'option2') - - // Should emit Vue event - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('option2') + expect(callback).toHaveBeenCalledExactlyOnceWith('custom_b') }) it('handles value changes gracefully', async () => { - const widget = createMockWidget('option1') + const callback = vi.fn() + const widget = createMockWidget('option1', {}, callback) const wrapper = mountComponent(widget, 'option1') - const emitted = await setSelectValueAndEmit(wrapper, 'option2') + await setSelectValue(wrapper, 'option2') - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('option2') + expect(callback).toHaveBeenCalledExactlyOnceWith('option2') }) }) @@ -176,43 +166,43 @@ describe('WidgetSelect Value Binding', () => { 'option@#$%', 'option/with\\slashes' ] - const widget = createMockWidget(specialOptions[0], { - values: specialOptions - }) + const callback = vi.fn() + const widget = createMockWidget( + specialOptions[0], + { + values: specialOptions + }, + callback + ) const wrapper = mountComponent(widget, specialOptions[0]) - const emitted = await setSelectValueAndEmit(wrapper, specialOptions[1]) + await setSelectValue(wrapper, specialOptions[1]) - expect(emitted).toBeDefined() - expect(emitted![0]).toContain(specialOptions[1]) + expect(callback).toHaveBeenCalledExactlyOnceWith(specialOptions[1]) }) }) describe('Edge Cases', () => { it('handles selection of non-existent option gracefully', async () => { - const widget = createMockWidget('option1') + const callback = vi.fn() + const widget = createMockWidget('option1', {}, callback) const wrapper = mountComponent(widget, 'option1') - const emitted = await setSelectValueAndEmit( - wrapper, - 'non_existent_option' - ) + await setSelectValue(wrapper, 'non_existent_option') - // Should still emit Vue event with the value - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('non_existent_option') + // Should still trigger callback with the value + expect(callback).toHaveBeenCalledExactlyOnceWith('non_existent_option') }) it('handles numeric string options correctly', async () => { + const callback = vi.fn() const numericOptions = ['1', '2', '10', '100'] - const widget = createMockWidget('1', { values: numericOptions }) + const widget = createMockWidget('1', { values: numericOptions }, callback) const wrapper = mountComponent(widget, '1') - const emitted = await setSelectValueAndEmit(wrapper, '100') + await setSelectValue(wrapper, '100') - // Should maintain string type in emitted event - expect(emitted).toBeDefined() - expect(emitted![0]).toContain('100') + expect(callback).toHaveBeenCalledExactlyOnceWith('100') }) }) diff --git a/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts index 0bc25974b..cceee3795 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts @@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils' import type { VueWrapper } from '@vue/test-utils' import PrimeVue from 'primevue/config' import type { ComponentPublicInstance } from 'vue' -import { ref } from 'vue' +import { nextTick, ref, watch } from 'vue' import { describe, expect, it, vi } from 'vitest' import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' @@ -25,17 +25,22 @@ describe('WidgetSelectDropdown custom label mapping', () => { values?: string[] getOptionLabel?: (value: string | null) => string } = {}, - spec?: ComboInputSpec - ): SimplifiedWidget => ({ - name: 'test_image_select', - type: 'combo', - value: () => ref(value), - options: { - values: ['img_001.png', 'photo_abc.jpg', 'hash789.png'], - ...options - }, - spec - }) + spec?: ComboInputSpec, + callback?: (value: string | undefined) => void + ): SimplifiedWidget => { + const valueRef = ref(value) + if (callback) watch(valueRef, (v) => callback(v)) + return { + name: 'test_image_select', + type: 'combo', + value: () => valueRef, + options: { + values: ['img_001.png', 'photo_abc.jpg', 'hash789.png'], + ...options + }, + spec + } + } const mountComponent = ( widget: SimplifiedWidget, @@ -103,26 +108,29 @@ describe('WidgetSelectDropdown custom label mapping', () => { expect(getOptionLabel).toHaveBeenCalledWith('hash789.png') }) - it('emits original values when items with custom labels are selected', async () => { + it('triggers callback with original values when items with custom labels are selected', async () => { const getOptionLabel = vi.fn((value: string | null) => { if (!value) return 'No file' return `Custom: ${value}` }) - const widget = createMockWidget('img_001.png', { - getOptionLabel - }) + const callback = vi.fn() + const widget = createMockWidget( + 'img_001.png', + { + getOptionLabel + }, + undefined, + callback + ) const wrapper = mountComponent(widget, 'img_001.png') // Simulate selecting an item const selectedSet = new Set(['input-1']) // index 1 = photo_abc.jpg wrapper.vm.updateSelectedItems(selectedSet) - // Should emit the original value, not the custom label - expect(wrapper.emitted('update:modelValue')).toBeDefined() - expect(wrapper.emitted('update:modelValue')![0]).toEqual([ - 'photo_abc.jpg' - ]) + await nextTick() + expect(callback).toHaveBeenCalledWith('photo_abc.jpg') }) it('falls back to original value when label mapping fails', () => {