Skip to content

Commit 0d3b155

Browse files
[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.
1 parent 5c3b67b commit 0d3b155

File tree

4 files changed

+671
-0
lines changed

4 files changed

+671
-0
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { mount } from '@vue/test-utils'
2+
import PrimeVue from 'primevue/config'
3+
import InputText from 'primevue/inputtext'
4+
import type { InputTextProps } from 'primevue/inputtext'
5+
import Textarea from 'primevue/textarea'
6+
import { describe, expect, it } from 'vitest'
7+
8+
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
9+
10+
import WidgetInputText from './WidgetInputText.vue'
11+
12+
describe('WidgetInputText Value Binding', () => {
13+
const createMockWidget = (
14+
value: string = 'default',
15+
options: Partial<InputTextProps> = {},
16+
callback?: (value: string) => void
17+
): SimplifiedWidget<string> => ({
18+
name: 'test_input',
19+
type: 'string',
20+
value,
21+
options,
22+
callback
23+
})
24+
25+
const mountComponent = (
26+
widget: SimplifiedWidget<string>,
27+
modelValue: string,
28+
readonly = false
29+
) => {
30+
return mount(WidgetInputText, {
31+
global: {
32+
plugins: [PrimeVue],
33+
components: { InputText, Textarea }
34+
},
35+
props: {
36+
widget,
37+
modelValue,
38+
readonly
39+
}
40+
})
41+
}
42+
43+
const setInputValueAndTrigger = async (
44+
wrapper: ReturnType<typeof mount>,
45+
value: string,
46+
trigger: 'blur' | 'keydown.enter' = 'blur'
47+
) => {
48+
const input = wrapper.find('input[type="text"]')
49+
if (!(input.element instanceof HTMLInputElement)) {
50+
throw new Error('Input element not found or is not an HTMLInputElement')
51+
}
52+
await input.setValue(value)
53+
await input.trigger(trigger)
54+
return input
55+
}
56+
57+
describe('Vue Event Emission', () => {
58+
it('emits Vue event when input value changes on blur', async () => {
59+
const widget = createMockWidget('hello')
60+
const wrapper = mountComponent(widget, 'hello')
61+
62+
await setInputValueAndTrigger(wrapper, 'world', 'blur')
63+
64+
const emitted = wrapper.emitted('update:modelValue')
65+
expect(emitted).toBeDefined()
66+
expect(emitted![0]).toContain('world')
67+
})
68+
69+
it('emits Vue event when enter key is pressed', async () => {
70+
const widget = createMockWidget('initial')
71+
const wrapper = mountComponent(widget, 'initial')
72+
73+
await setInputValueAndTrigger(wrapper, 'new value', 'keydown.enter')
74+
75+
const emitted = wrapper.emitted('update:modelValue')
76+
expect(emitted).toBeDefined()
77+
expect(emitted![0]).toContain('new value')
78+
})
79+
80+
it('handles empty string values', async () => {
81+
const widget = createMockWidget('something')
82+
const wrapper = mountComponent(widget, 'something')
83+
84+
await setInputValueAndTrigger(wrapper, '')
85+
86+
const emitted = wrapper.emitted('update:modelValue')
87+
expect(emitted).toBeDefined()
88+
expect(emitted![0]).toContain('')
89+
})
90+
91+
it('handles special characters correctly', async () => {
92+
const widget = createMockWidget('normal')
93+
const wrapper = mountComponent(widget, 'normal')
94+
95+
const specialText = 'special @#$%^&*()[]{}|\\:";\'<>?,./'
96+
await setInputValueAndTrigger(wrapper, specialText)
97+
98+
const emitted = wrapper.emitted('update:modelValue')
99+
expect(emitted).toBeDefined()
100+
expect(emitted![0]).toContain(specialText)
101+
})
102+
103+
it('handles missing callback gracefully', async () => {
104+
const widget = createMockWidget('test', {}, undefined)
105+
const wrapper = mountComponent(widget, 'test')
106+
107+
await setInputValueAndTrigger(wrapper, 'new value')
108+
109+
// Should still emit Vue event
110+
const emitted = wrapper.emitted('update:modelValue')
111+
expect(emitted).toBeDefined()
112+
expect(emitted![0]).toContain('new value')
113+
})
114+
})
115+
116+
describe('User Interactions', () => {
117+
it('emits update:modelValue on blur', async () => {
118+
const widget = createMockWidget('original')
119+
const wrapper = mountComponent(widget, 'original')
120+
121+
await setInputValueAndTrigger(wrapper, 'updated')
122+
123+
const emitted = wrapper.emitted('update:modelValue')
124+
expect(emitted).toBeDefined()
125+
expect(emitted![0]).toContain('updated')
126+
})
127+
128+
it('emits update:modelValue on enter key', async () => {
129+
const widget = createMockWidget('start')
130+
const wrapper = mountComponent(widget, 'start')
131+
132+
await setInputValueAndTrigger(wrapper, 'finish', 'keydown.enter')
133+
134+
const emitted = wrapper.emitted('update:modelValue')
135+
expect(emitted).toBeDefined()
136+
expect(emitted![0]).toContain('finish')
137+
})
138+
})
139+
140+
describe('Readonly Mode', () => {
141+
it('disables input when readonly', () => {
142+
const widget = createMockWidget('readonly test')
143+
const wrapper = mountComponent(widget, 'readonly test', true)
144+
145+
const input = wrapper.find('input[type="text"]')
146+
if (!(input.element instanceof HTMLInputElement)) {
147+
throw new Error('Input element not found or is not an HTMLInputElement')
148+
}
149+
expect(input.element.disabled).toBe(true)
150+
})
151+
})
152+
153+
describe('Component Rendering', () => {
154+
it('always renders InputText component', () => {
155+
const widget = createMockWidget('test value')
156+
const wrapper = mountComponent(widget, 'test value')
157+
158+
// WidgetInputText always uses InputText, not Textarea
159+
const input = wrapper.find('input[type="text"]')
160+
expect(input.exists()).toBe(true)
161+
162+
// Should not render textarea (that's handled by WidgetTextarea component)
163+
const textarea = wrapper.find('textarea')
164+
expect(textarea.exists()).toBe(false)
165+
})
166+
})
167+
168+
describe('Edge Cases', () => {
169+
it('handles very long strings', async () => {
170+
const widget = createMockWidget('short')
171+
const wrapper = mountComponent(widget, 'short')
172+
173+
const longString = 'a'.repeat(10000)
174+
await setInputValueAndTrigger(wrapper, longString)
175+
176+
const emitted = wrapper.emitted('update:modelValue')
177+
expect(emitted).toBeDefined()
178+
expect(emitted![0]).toContain(longString)
179+
})
180+
181+
it('handles unicode characters', async () => {
182+
const widget = createMockWidget('ascii')
183+
const wrapper = mountComponent(widget, 'ascii')
184+
185+
const unicodeText = '🎨 Unicode: αβγ 中文 العربية 🚀'
186+
await setInputValueAndTrigger(wrapper, unicodeText)
187+
188+
const emitted = wrapper.emitted('update:modelValue')
189+
expect(emitted).toBeDefined()
190+
expect(emitted![0]).toContain(unicodeText)
191+
})
192+
})
193+
})
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { mount } from '@vue/test-utils'
2+
import PrimeVue from 'primevue/config'
3+
import Select from 'primevue/select'
4+
import type { SelectProps } from 'primevue/select'
5+
import { describe, expect, it } from 'vitest'
6+
7+
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
8+
9+
import WidgetSelect from './WidgetSelect.vue'
10+
11+
describe('WidgetSelect Value Binding', () => {
12+
const createMockWidget = (
13+
value: string = 'option1',
14+
options: Partial<
15+
SelectProps & { values?: string[]; return_index?: boolean }
16+
> = {},
17+
callback?: (value: string | number | undefined) => void
18+
): SimplifiedWidget<string | number | undefined> => ({
19+
name: 'test_select',
20+
type: 'combo',
21+
value,
22+
options: {
23+
values: ['option1', 'option2', 'option3'],
24+
...options
25+
},
26+
callback
27+
})
28+
29+
const mountComponent = (
30+
widget: SimplifiedWidget<string | number | undefined>,
31+
modelValue: string | number | undefined,
32+
readonly = false
33+
) => {
34+
return mount(WidgetSelect, {
35+
props: {
36+
widget,
37+
modelValue,
38+
readonly
39+
},
40+
global: {
41+
plugins: [PrimeVue],
42+
components: { Select }
43+
}
44+
})
45+
}
46+
47+
const setSelectValueAndEmit = async (
48+
wrapper: ReturnType<typeof mount>,
49+
value: string
50+
) => {
51+
const select = wrapper.findComponent({ name: 'Select' })
52+
await select.setValue(value)
53+
return wrapper.emitted('update:modelValue')
54+
}
55+
56+
describe('Vue Event Emission', () => {
57+
it('emits Vue event when selection changes', async () => {
58+
const widget = createMockWidget('option1')
59+
const wrapper = mountComponent(widget, 'option1')
60+
61+
const emitted = await setSelectValueAndEmit(wrapper, 'option2')
62+
63+
expect(emitted).toBeDefined()
64+
expect(emitted![0]).toContain('option2')
65+
})
66+
67+
it('emits string value for different options', async () => {
68+
const widget = createMockWidget('option1')
69+
const wrapper = mountComponent(widget, 'option1')
70+
71+
const emitted = await setSelectValueAndEmit(wrapper, 'option3')
72+
73+
expect(emitted).toBeDefined()
74+
// Should emit the string value
75+
expect(emitted![0]).toContain('option3')
76+
})
77+
78+
it('handles custom option values', async () => {
79+
const customOptions = ['custom_a', 'custom_b', 'custom_c']
80+
const widget = createMockWidget('custom_a', { values: customOptions })
81+
const wrapper = mountComponent(widget, 'custom_a')
82+
83+
const emitted = await setSelectValueAndEmit(wrapper, 'custom_b')
84+
85+
expect(emitted).toBeDefined()
86+
expect(emitted![0]).toContain('custom_b')
87+
})
88+
89+
it('handles missing callback gracefully', async () => {
90+
const widget = createMockWidget('option1', {}, undefined)
91+
const wrapper = mountComponent(widget, 'option1')
92+
93+
const emitted = await setSelectValueAndEmit(wrapper, 'option2')
94+
95+
// Should emit Vue event
96+
expect(emitted).toBeDefined()
97+
expect(emitted![0]).toContain('option2')
98+
})
99+
100+
it('handles value changes gracefully', async () => {
101+
const widget = createMockWidget('option1')
102+
const wrapper = mountComponent(widget, 'option1')
103+
104+
const emitted = await setSelectValueAndEmit(wrapper, 'option2')
105+
106+
expect(emitted).toBeDefined()
107+
expect(emitted![0]).toContain('option2')
108+
})
109+
})
110+
111+
describe('Readonly Mode', () => {
112+
it('disables the select component when readonly', async () => {
113+
const widget = createMockWidget('option1')
114+
const wrapper = mountComponent(widget, 'option1', true)
115+
116+
const select = wrapper.findComponent({ name: 'Select' })
117+
expect(select.props('disabled')).toBe(true)
118+
})
119+
})
120+
121+
describe('Option Handling', () => {
122+
it('handles empty options array', async () => {
123+
const widget = createMockWidget('', { values: [] })
124+
const wrapper = mountComponent(widget, '')
125+
126+
const select = wrapper.findComponent({ name: 'Select' })
127+
expect(select.props('options')).toEqual([])
128+
})
129+
130+
it('handles single option', async () => {
131+
const widget = createMockWidget('only_option', {
132+
values: ['only_option']
133+
})
134+
const wrapper = mountComponent(widget, 'only_option')
135+
136+
const select = wrapper.findComponent({ name: 'Select' })
137+
const options = select.props('options')
138+
expect(options).toHaveLength(1)
139+
expect(options[0]).toEqual('only_option')
140+
})
141+
142+
it('handles options with special characters', async () => {
143+
const specialOptions = [
144+
'option with spaces',
145+
'option@#$%',
146+
'option/with\\slashes'
147+
]
148+
const widget = createMockWidget(specialOptions[0], {
149+
values: specialOptions
150+
})
151+
const wrapper = mountComponent(widget, specialOptions[0])
152+
153+
const emitted = await setSelectValueAndEmit(wrapper, specialOptions[1])
154+
155+
expect(emitted).toBeDefined()
156+
expect(emitted![0]).toContain(specialOptions[1])
157+
})
158+
})
159+
160+
describe('Edge Cases', () => {
161+
it('handles selection of non-existent option gracefully', async () => {
162+
const widget = createMockWidget('option1')
163+
const wrapper = mountComponent(widget, 'option1')
164+
165+
const emitted = await setSelectValueAndEmit(
166+
wrapper,
167+
'non_existent_option'
168+
)
169+
170+
// Should still emit Vue event with the value
171+
expect(emitted).toBeDefined()
172+
expect(emitted![0]).toContain('non_existent_option')
173+
})
174+
175+
it('handles numeric string options correctly', async () => {
176+
const numericOptions = ['1', '2', '10', '100']
177+
const widget = createMockWidget('1', { values: numericOptions })
178+
const wrapper = mountComponent(widget, '1')
179+
180+
const emitted = await setSelectValueAndEmit(wrapper, '100')
181+
182+
// Should maintain string type in emitted event
183+
expect(emitted).toBeDefined()
184+
expect(emitted![0]).toContain('100')
185+
})
186+
})
187+
})

0 commit comments

Comments
 (0)