[test] Add component tests for some Vue Widget components (#5409)

* add component tests for vue widgets

* [refactor] improve widget test readability and type safety - addresses @DrJKL's review feedback

- Add mountComponent utility function for consistent test setup
- Add setInputValueAndTrigger helper to batch common test operations
- Replace type assertions with proper instanceof checks for type safety
- Remove duplicate test setup code to improve test readability
- Fix TypeScript errors in WidgetSlider tests

These changes address all review comments by making tests easier to read
and understand while ensuring proper type checking.

* [refactor] apply consistent test patterns to WidgetSelect.test.ts

- Add mountComponent utility function for consistent test setup
- Add setSelectValueAndEmit helper to batch select operations
- Remove repetitive mount boilerplate code throughout tests
- Maintain existing test coverage while improving readability

This ensures all widget component tests follow the same patterns
established in WidgetInputText and WidgetSlider tests.
This commit is contained in:
Christian Byrne
2025-09-06 23:08:54 -07:00
committed by snomiao
parent 1b843e9a3a
commit f468a0cf6b
4 changed files with 671 additions and 0 deletions

View File

@@ -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<InputTextProps> = {},
callback?: (value: string) => void
): SimplifiedWidget<string> => ({
name: 'test_input',
type: 'string',
value,
options,
callback
})
const mountComponent = (
widget: SimplifiedWidget<string>,
modelValue: string,
readonly = false
) => {
return mount(WidgetInputText, {
global: {
plugins: [PrimeVue],
components: { InputText, Textarea }
},
props: {
widget,
modelValue,
readonly
}
})
}
const setInputValueAndTrigger = async (
wrapper: ReturnType<typeof mount>,
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)
})
})
})

View File

@@ -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<string | number | undefined> => ({
name: 'test_select',
type: 'combo',
value,
options: {
values: ['option1', 'option2', 'option3'],
...options
},
callback
})
const mountComponent = (
widget: SimplifiedWidget<string | number | undefined>,
modelValue: string | number | undefined,
readonly = false
) => {
return mount(WidgetSelect, {
props: {
widget,
modelValue,
readonly
},
global: {
plugins: [PrimeVue],
components: { Select }
}
})
}
const setSelectValueAndEmit = async (
wrapper: ReturnType<typeof mount>,
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')
})
})
})

View File

@@ -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<SliderProps & { precision?: number }> = {},
callback?: (value: number) => void
): SimplifiedWidget<number> => ({
name: 'test_slider',
type: 'float',
value,
options: { min: 0, max: 100, step: 1, precision: 0, ...options },
callback
})
const mountComponent = (
widget: SimplifiedWidget<number>,
modelValue: number,
readonly = false
) => {
return mount(WidgetSlider, {
global: {
plugins: [PrimeVue],
components: { InputText, Slider }
},
props: {
widget,
modelValue,
readonly
}
})
}
const getNumberInput = (wrapper: ReturnType<typeof mount>) => {
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)
})
})
})

View File

@@ -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<ToggleSwitchProps> = {},
callback?: (value: boolean) => void
): SimplifiedWidget<boolean> => ({
name: 'test_toggle',
type: 'boolean',
value,
options,
callback
})
const mountComponent = (
widget: SimplifiedWidget<boolean>,
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)
})
})
})