diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.test.ts new file mode 100644 index 000000000..8942bea34 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.test.ts @@ -0,0 +1,193 @@ +import { mount } from '@vue/test-utils' +import PrimeVue from 'primevue/config' +import InputText from 'primevue/inputtext' +import type { InputTextProps } from 'primevue/inputtext' +import Textarea from 'primevue/textarea' +import { describe, expect, it } from 'vitest' + +import type { SimplifiedWidget } from '@/types/simplifiedWidget' + +import WidgetInputText from './WidgetInputText.vue' + +describe('WidgetInputText Value Binding', () => { + const createMockWidget = ( + value: string = 'default', + options: Partial = {}, + callback?: (value: string) => void + ): SimplifiedWidget => ({ + name: 'test_input', + type: 'string', + value, + options, + callback + }) + + const mountComponent = ( + widget: SimplifiedWidget, + modelValue: string, + readonly = false + ) => { + return mount(WidgetInputText, { + global: { + plugins: [PrimeVue], + components: { InputText, Textarea } + }, + props: { + widget, + modelValue, + readonly + } + }) + } + + const setInputValueAndTrigger = async ( + wrapper: ReturnType, + value: string, + trigger: 'blur' | 'keydown.enter' = 'blur' + ) => { + const input = wrapper.find('input[type="text"]') + if (!(input.element instanceof HTMLInputElement)) { + throw new Error('Input element not found or is not an HTMLInputElement') + } + await input.setValue(value) + await input.trigger(trigger) + 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') + }) + + it('handles empty string values', async () => { + const widget = createMockWidget('something') + const wrapper = mountComponent(widget, 'something') + + await setInputValueAndTrigger(wrapper, '') + + const emitted = wrapper.emitted('update:modelValue') + expect(emitted).toBeDefined() + expect(emitted![0]).toContain('') + }) + + it('handles special characters correctly', async () => { + const widget = createMockWidget('normal') + 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') + }) + }) + + describe('Readonly Mode', () => { + it('disables input when readonly', () => { + const widget = createMockWidget('readonly test') + const wrapper = mountComponent(widget, 'readonly test', true) + + const input = wrapper.find('input[type="text"]') + if (!(input.element instanceof HTMLInputElement)) { + throw new Error('Input element not found or is not an HTMLInputElement') + } + expect(input.element.disabled).toBe(true) + }) + }) + + describe('Component Rendering', () => { + it('always renders InputText component', () => { + const widget = createMockWidget('test value') + const wrapper = mountComponent(widget, 'test value') + + // WidgetInputText always uses InputText, not Textarea + const input = wrapper.find('input[type="text"]') + expect(input.exists()).toBe(true) + + // Should not render textarea (that's handled by WidgetTextarea component) + const textarea = wrapper.find('textarea') + expect(textarea.exists()).toBe(false) + }) + }) + + describe('Edge Cases', () => { + it('handles very long strings', async () => { + const widget = createMockWidget('short') + 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) + }) + + it('handles unicode characters', async () => { + const widget = createMockWidget('ascii') + 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) + }) + }) +}) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts new file mode 100644 index 000000000..28f950360 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts @@ -0,0 +1,187 @@ +import { mount } from '@vue/test-utils' +import PrimeVue from 'primevue/config' +import Select from 'primevue/select' +import type { SelectProps } from 'primevue/select' +import { describe, expect, it } from 'vitest' + +import type { SimplifiedWidget } from '@/types/simplifiedWidget' + +import WidgetSelect from './WidgetSelect.vue' + +describe('WidgetSelect Value Binding', () => { + const createMockWidget = ( + value: string = 'option1', + options: Partial< + SelectProps & { values?: string[]; return_index?: boolean } + > = {}, + callback?: (value: string | number | undefined) => void + ): SimplifiedWidget => ({ + name: 'test_select', + type: 'combo', + value, + options: { + values: ['option1', 'option2', 'option3'], + ...options + }, + callback + }) + + const mountComponent = ( + widget: SimplifiedWidget, + modelValue: string | number | undefined, + readonly = false + ) => { + return mount(WidgetSelect, { + props: { + widget, + modelValue, + readonly + }, + global: { + plugins: [PrimeVue], + components: { Select } + } + }) + } + + const setSelectValueAndEmit = 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') + const wrapper = mountComponent(widget, 'option1') + + const emitted = await setSelectValueAndEmit(wrapper, 'option2') + + expect(emitted).toBeDefined() + expect(emitted![0]).toContain('option2') + }) + + it('emits string value for different options', async () => { + const widget = createMockWidget('option1') + const wrapper = mountComponent(widget, 'option1') + + const emitted = await setSelectValueAndEmit(wrapper, 'option3') + + expect(emitted).toBeDefined() + // Should emit the string value + expect(emitted![0]).toContain('option3') + }) + + it('handles custom option values', async () => { + const customOptions = ['custom_a', 'custom_b', 'custom_c'] + const widget = createMockWidget('custom_a', { values: customOptions }) + const wrapper = mountComponent(widget, 'custom_a') + + const emitted = await setSelectValueAndEmit(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') + }) + + it('handles value changes gracefully', async () => { + const widget = createMockWidget('option1') + const wrapper = mountComponent(widget, 'option1') + + const emitted = await setSelectValueAndEmit(wrapper, 'option2') + + expect(emitted).toBeDefined() + expect(emitted![0]).toContain('option2') + }) + }) + + describe('Readonly Mode', () => { + it('disables the select component when readonly', async () => { + const widget = createMockWidget('option1') + const wrapper = mountComponent(widget, 'option1', true) + + const select = wrapper.findComponent({ name: 'Select' }) + expect(select.props('disabled')).toBe(true) + }) + }) + + describe('Option Handling', () => { + it('handles empty options array', async () => { + const widget = createMockWidget('', { values: [] }) + const wrapper = mountComponent(widget, '') + + const select = wrapper.findComponent({ name: 'Select' }) + expect(select.props('options')).toEqual([]) + }) + + it('handles single option', async () => { + const widget = createMockWidget('only_option', { + values: ['only_option'] + }) + const wrapper = mountComponent(widget, 'only_option') + + const select = wrapper.findComponent({ name: 'Select' }) + const options = select.props('options') + expect(options).toHaveLength(1) + expect(options[0]).toEqual('only_option') + }) + + it('handles options with special characters', async () => { + const specialOptions = [ + 'option with spaces', + 'option@#$%', + 'option/with\\slashes' + ] + const widget = createMockWidget(specialOptions[0], { + values: specialOptions + }) + const wrapper = mountComponent(widget, specialOptions[0]) + + const emitted = await setSelectValueAndEmit(wrapper, specialOptions[1]) + + expect(emitted).toBeDefined() + expect(emitted![0]).toContain(specialOptions[1]) + }) + }) + + describe('Edge Cases', () => { + it('handles selection of non-existent option gracefully', async () => { + const widget = createMockWidget('option1') + const wrapper = mountComponent(widget, 'option1') + + const emitted = await setSelectValueAndEmit( + wrapper, + 'non_existent_option' + ) + + // Should still emit Vue event with the value + expect(emitted).toBeDefined() + expect(emitted![0]).toContain('non_existent_option') + }) + + it('handles numeric string options correctly', async () => { + const numericOptions = ['1', '2', '10', '100'] + const widget = createMockWidget('1', { values: numericOptions }) + const wrapper = mountComponent(widget, '1') + + const emitted = await setSelectValueAndEmit(wrapper, '100') + + // Should maintain string type in emitted event + expect(emitted).toBeDefined() + expect(emitted![0]).toContain('100') + }) + }) +}) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSlider.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetSlider.test.ts new file mode 100644 index 000000000..3e554023f --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSlider.test.ts @@ -0,0 +1,131 @@ +import { mount } from '@vue/test-utils' +import PrimeVue from 'primevue/config' +import InputText from 'primevue/inputtext' +import Slider from 'primevue/slider' +import type { SliderProps } from 'primevue/slider' +import { describe, expect, it } from 'vitest' + +import type { SimplifiedWidget } from '@/types/simplifiedWidget' + +import WidgetSlider from './WidgetSlider.vue' + +describe('WidgetSlider Value Binding', () => { + const createMockWidget = ( + value: number = 5, + options: Partial = {}, + callback?: (value: number) => void + ): SimplifiedWidget => ({ + name: 'test_slider', + type: 'float', + value, + options: { min: 0, max: 100, step: 1, precision: 0, ...options }, + callback + }) + + const mountComponent = ( + widget: SimplifiedWidget, + modelValue: number, + readonly = false + ) => { + return mount(WidgetSlider, { + global: { + plugins: [PrimeVue], + components: { InputText, Slider } + }, + props: { + widget, + modelValue, + readonly + } + }) + } + + const getNumberInput = (wrapper: ReturnType) => { + const input = wrapper.find('input[type="number"]') + if (!(input.element instanceof HTMLInputElement)) { + throw new Error( + 'Number input element not found or is not an HTMLInputElement' + ) + } + return { element: input.element } + } + + describe('Props and Values', () => { + it('passes modelValue to slider component', () => { + const widget = createMockWidget(5) + const wrapper = mountComponent(widget, 5) + + const slider = wrapper.findComponent({ name: 'Slider' }) + expect(slider.props('modelValue')).toBe(5) + }) + + it('handles different initial values', () => { + const widget1 = createMockWidget(5) + const wrapper1 = mountComponent(widget1, 5) + + const widget2 = createMockWidget(10) + const wrapper2 = mountComponent(widget2, 10) + + const slider1 = wrapper1.findComponent({ name: 'Slider' }) + expect(slider1.props('modelValue')).toBe(5) + + const slider2 = wrapper2.findComponent({ name: 'Slider' }) + expect(slider2.props('modelValue')).toBe(10) + }) + }) + + describe('Component Rendering', () => { + it('renders slider component', () => { + const widget = createMockWidget(5) + const wrapper = mountComponent(widget, 5) + + expect(wrapper.findComponent({ name: 'Slider' }).exists()).toBe(true) + }) + + it('renders input field', () => { + const widget = createMockWidget(5) + const wrapper = mountComponent(widget, 5) + + expect(wrapper.find('input[type="number"]').exists()).toBe(true) + }) + + it('displays initial value in input field', () => { + const widget = createMockWidget(42) + const wrapper = mountComponent(widget, 42) + + const input = getNumberInput(wrapper) + expect(input.element.value).toBe('42') + }) + + it('disables components in readonly mode', () => { + const widget = createMockWidget(5) + const wrapper = mountComponent(widget, 5, true) + + const slider = wrapper.findComponent({ name: 'Slider' }) + expect(slider.props('disabled')).toBe(true) + + const input = getNumberInput(wrapper) + expect(input.element.disabled).toBe(true) + }) + }) + + describe('Widget Options', () => { + it('passes widget options to PrimeVue components', () => { + const widget = createMockWidget(5, { min: -10, max: 50 }) + const wrapper = mountComponent(widget, 5) + + const slider = wrapper.findComponent({ name: 'Slider' }) + expect(slider.props('min')).toBe(-10) + expect(slider.props('max')).toBe(50) + }) + + it('handles negative value ranges', () => { + const widget = createMockWidget(0, { min: -100, max: 100 }) + const wrapper = mountComponent(widget, 0) + + const slider = wrapper.findComponent({ name: 'Slider' }) + expect(slider.props('min')).toBe(-100) + expect(slider.props('max')).toBe(100) + }) + }) +}) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts new file mode 100644 index 000000000..b7efcbcd5 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts @@ -0,0 +1,160 @@ +import { mount } from '@vue/test-utils' +import PrimeVue from 'primevue/config' +import ToggleSwitch from 'primevue/toggleswitch' +import type { ToggleSwitchProps } from 'primevue/toggleswitch' +import { describe, expect, it } from 'vitest' + +import type { SimplifiedWidget } from '@/types/simplifiedWidget' + +import WidgetToggleSwitch from './WidgetToggleSwitch.vue' + +describe('WidgetToggleSwitch Value Binding', () => { + const createMockWidget = ( + value: boolean = false, + options: Partial = {}, + callback?: (value: boolean) => void + ): SimplifiedWidget => ({ + name: 'test_toggle', + type: 'boolean', + value, + options, + callback + }) + + const mountComponent = ( + widget: SimplifiedWidget, + modelValue: boolean, + readonly = false + ) => { + return mount(WidgetToggleSwitch, { + props: { + widget, + modelValue, + readonly + }, + global: { + plugins: [PrimeVue], + components: { ToggleSwitch } + } + }) + } + + 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) + const wrapper = mountComponent(widget, false) + + expect(wrapper.findComponent({ name: 'ToggleSwitch' }).exists()).toBe( + true + ) + }) + + it('displays correct initial state for false', () => { + const widget = createMockWidget(false) + const wrapper = mountComponent(widget, false) + + const toggle = wrapper.findComponent({ name: 'ToggleSwitch' }) + expect(toggle.props('modelValue')).toBe(false) + }) + + it('displays correct initial state for true', () => { + const widget = createMockWidget(true) + const wrapper = mountComponent(widget, true) + + const toggle = wrapper.findComponent({ name: 'ToggleSwitch' }) + expect(toggle.props('modelValue')).toBe(true) + }) + + it('disables component in readonly mode', () => { + const widget = createMockWidget(false) + const wrapper = mountComponent(widget, false, true) + + const toggle = wrapper.findComponent({ name: 'ToggleSwitch' }) + expect(toggle.props('disabled')).toBe(true) + }) + }) + + 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 wrapper = mountComponent(widget, false) + + const toggle = wrapper.findComponent({ name: 'ToggleSwitch' }) + + // Multiple state changes + await toggle.setValue(true) + await toggle.setValue(false) + await toggle.setValue(true) + await toggle.setValue(false) + + const emitted = wrapper.emitted('update:modelValue') + expect(emitted).toHaveLength(4) + // Verify alternating pattern + expect(emitted![0]).toContain(true) + expect(emitted![1]).toContain(false) + expect(emitted![2]).toContain(true) + expect(emitted![3]).toContain(false) + }) + }) +})