Fix unit tests

This commit is contained in:
Austin Mroz
2025-12-02 11:55:10 -08:00
parent 948320137e
commit a07097e25d
10 changed files with 164 additions and 353 deletions

View File

@@ -87,6 +87,34 @@ export interface GraphNodeManager {
cleanup(): void 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<ControlOptions>)|undefined { function getControlWidget(widget: IBaseWidget): (() => Ref<ControlOptions>)|undefined {
const cagWidget = widget.linkedWidgets?.find( const cagWidget = widget.linkedWidgets?.find(
(w) => w.name == 'control_after_generate' (w) => w.name == 'control_after_generate'
@@ -127,7 +155,7 @@ export function safeWidgetMapper(
}) })
widget.callback = useChainCallback(widget.callback, () => { widget.callback = useChainCallback(widget.callback, () => {
if (valueRef.value !== widget.value) if (valueRef.value !== widget.value)
valueRef.value = validateWidgetValue(widget.value) ?? undefined valueRef.value = normalizeWidgetValue(widget.value) ?? undefined
}) })
widget.valueRef = () => valueRef widget.valueRef = () => valueRef
} }

View File

@@ -199,11 +199,7 @@ describe('WidgetButton Interactions', () => {
const widget = createMockWidget({}, mockCallback) const widget = createMockWidget({}, mockCallback)
const wrapper = mountComponent(widget) const wrapper = mountComponent(widget)
// Simulate rapid clicks for (let i = 0; i < 16; i++) await clickButton(wrapper)
const clickPromises = Array.from({ length: 16 }, () =>
clickButton(wrapper)
)
await Promise.all(clickPromises)
expect(mockCallback).toHaveBeenCalledTimes(16) expect(mockCallback).toHaveBeenCalledTimes(16)
}) })

View File

@@ -16,7 +16,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from 'primevue/button' import Button from 'primevue/button'
import { computed, triggerRef } from 'vue' import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget' import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { import {
@@ -37,7 +37,8 @@ const filteredProps = computed(() =>
) )
const handleClick = () => { const handleClick = () => {
//FIXME: Will do nothing since backing value is unchanged const ref = props.widget.value()
triggerRef(props.widget.value()) //@ts-expect-error - need to actually assign value, can't use triggerRef :(
ref.value = !ref.value
} }
</script> </script>

View File

@@ -15,7 +15,7 @@ function createMockWidget(
callback?: (value: number) => void callback?: (value: number) => void
): SimplifiedWidget<number> { ): SimplifiedWidget<number> {
const valueRef = ref(value) const valueRef = ref(value)
if (callback) watch(valueRef, callback) if (callback) watch(valueRef, (v) => callback(v))
return { return {
name: 'test_input_number', name: 'test_input_number',
type, type,
@@ -52,15 +52,14 @@ describe('WidgetInputNumberInput Value Binding', () => {
}) })
it('emits update:modelValue when value changes', async () => { 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 wrapper = mountComponent(widget, 10)
const inputNumber = wrapper.findComponent(InputNumber) const inputNumber = wrapper.findComponent(InputNumber)
await inputNumber.vm.$emit('update:modelValue', 20) await inputNumber.setValue(20)
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith(20)
expect(emitted).toBeDefined()
expect(emitted![0]).toContain(20)
}) })
it('handles negative values', () => { it('handles negative values', () => {

View File

@@ -17,7 +17,7 @@ describe('WidgetInputText Value Binding', () => {
callback?: (value: string) => void callback?: (value: string) => void
): SimplifiedWidget<string> => { ): SimplifiedWidget<string> => {
const valueRef = ref(value) const valueRef = ref(value)
if (callback) watch(valueRef, callback) if (callback) watch(valueRef, (v) => callback(v))
return { return {
name: 'test_input', name: 'test_input',
type: 'string', type: 'string',
@@ -58,86 +58,26 @@ describe('WidgetInputText Value Binding', () => {
return input return input
} }
describe('Vue Event Emission', () => { describe('Widget Value Callbacks', () => {
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 () => { it('handles empty string values', async () => {
const widget = createMockWidget('something') const callback = vi.fn()
const widget = createMockWidget('something', {}, callback)
const wrapper = mountComponent(widget, 'something') const wrapper = mountComponent(widget, 'something')
await setInputValueAndTrigger(wrapper, '') await setInputValueAndTrigger(wrapper, '')
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith('')
expect(emitted).toBeDefined()
expect(emitted![0]).toContain('')
}) })
it('handles special characters correctly', async () => { 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 wrapper = mountComponent(widget, 'normal')
const specialText = 'special @#$%^&*()[]{}|\\:";\'<>?,./' const specialText = 'special @#$%^&*()[]{}|\\:";\'<>?,./'
await setInputValueAndTrigger(wrapper, specialText) await setInputValueAndTrigger(wrapper, specialText)
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith(specialText)
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')
}) })
}) })
@@ -158,27 +98,25 @@ describe('WidgetInputText Value Binding', () => {
describe('Edge Cases', () => { describe('Edge Cases', () => {
it('handles very long strings', async () => { 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 wrapper = mountComponent(widget, 'short')
const longString = 'a'.repeat(10000) const longString = 'a'.repeat(10000)
await setInputValueAndTrigger(wrapper, longString) await setInputValueAndTrigger(wrapper, longString)
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith(longString)
expect(emitted).toBeDefined()
expect(emitted![0]).toContain(longString)
}) })
it('handles unicode characters', async () => { it('handles unicode characters', async () => {
const widget = createMockWidget('ascii') const callback = vi.fn()
const widget = createMockWidget('ascii', {}, callback)
const wrapper = mountComponent(widget, 'ascii') const wrapper = mountComponent(widget, 'ascii')
const unicodeText = '🎨 Unicode: αβγ 中文 العربية 🚀' const unicodeText = '🎨 Unicode: αβγ 中文 العربية 🚀'
await setInputValueAndTrigger(wrapper, unicodeText) await setInputValueAndTrigger(wrapper, unicodeText)
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith(unicodeText)
expect(emitted).toBeDefined()
expect(emitted![0]).toContain(unicodeText)
}) })
}) })
}) })

View File

@@ -30,7 +30,7 @@ describe('WidgetMarkdown Dual Mode Display', () => {
callback?: (value: string) => void callback?: (value: string) => void
): SimplifiedWidget<string> => { ): SimplifiedWidget<string> => {
const valueRef = ref(value) const valueRef = ref(value)
if (callback) watch(valueRef, callback) if (callback) watch(valueRef, (v) => callback(v))
return { return {
name: 'test_markdown', name: 'test_markdown',
type: 'string', type: 'string',
@@ -213,7 +213,8 @@ describe('WidgetMarkdown Dual Mode Display', () => {
describe('Value Updates', () => { describe('Value Updates', () => {
it('emits update:modelValue when textarea content changes', async () => { 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') const wrapper = mountComponent(widget, '# Original')
await clickToEdit(wrapper) await clickToEdit(wrapper)
@@ -222,9 +223,7 @@ describe('WidgetMarkdown Dual Mode Display', () => {
await textarea.setValue('# Updated Content') await textarea.setValue('# Updated Content')
await textarea.trigger('input') await textarea.trigger('input')
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenLastCalledWith('# Updated Content')
expect(emitted).toBeDefined()
expect(emitted![emitted!.length - 1]).toEqual(['# Updated Content'])
}) })
it('renders updated HTML after value change and blur', async () => { it('renders updated HTML after value change and blur', async () => {
@@ -242,38 +241,6 @@ describe('WidgetMarkdown Dual Mode Display', () => {
expect(displayDiv.html()).toContain('<h2>New Heading</h2>') expect(displayDiv.html()).toContain('<h2>New Heading</h2>')
expect(displayDiv.html()).toContain('<strong>bold</strong>') expect(displayDiv.html()).toContain('<strong>bold</strong>')
}) })
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', () => { describe('Complex Markdown Rendering', () => {
@@ -340,8 +307,9 @@ Another line with more content.`
}) })
it('handles unicode characters', async () => { it('handles unicode characters', async () => {
const callback = vi.fn()
const unicode = '# Unicode: 🎨 αβγ 中文 العربية 🚀' const unicode = '# Unicode: 🎨 αβγ 中文 العربية 🚀'
const widget = createMockWidget(unicode) const widget = createMockWidget(unicode, {}, callback)
const wrapper = mountComponent(widget, unicode) const wrapper = mountComponent(widget, unicode)
await clickToEdit(wrapper) await clickToEdit(wrapper)
@@ -351,9 +319,7 @@ Another line with more content.`
await textarea.setValue(unicode + ' more unicode') await textarea.setValue(unicode + ' more unicode')
await textarea.trigger('input') await textarea.trigger('input')
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenLastCalledWith(unicode + ' more unicode')
expect(emitted).toBeDefined()
expect(emitted![emitted!.length - 1]).toEqual([unicode + ' more unicode'])
}) })
it('handles rapid edit mode toggling', async () => { it('handles rapid edit mode toggling', async () => {

View File

@@ -14,7 +14,7 @@ function createMockWidget(
callback?: (value: string) => void callback?: (value: string) => void
): SimplifiedWidget<string> { ): SimplifiedWidget<string> {
const valueRef = ref(value) const valueRef = ref(value)
if (callback) watch(valueRef, callback) if (callback) watch(valueRef, (v) => callback(v))
return { return {
name: 'test_textarea', name: 'test_textarea',
type: 'string', type: 'string',
@@ -60,98 +60,47 @@ async function setTextareaValueAndTrigger(
} }
describe('WidgetTextarea Value Binding', () => { describe('WidgetTextarea Value Binding', () => {
describe('Vue Event Emission', () => { describe('Widget Value Callbacks', () => {
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')
})
it('emits Vue event when textarea value changes on input', async () => { 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') const wrapper = mountComponent(widget, 'initial')
await setTextareaValueAndTrigger(wrapper, 'new content', 'input') await setTextareaValueAndTrigger(wrapper, 'new content', 'input')
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith('new content')
expect(emitted).toBeDefined()
expect(emitted?.[0]).toContain('new content')
}) })
it('handles empty string values', async () => { it('handles empty string values', async () => {
const widget = createMockWidget('something') const callback = vi.fn()
const widget = createMockWidget('something', {}, callback)
const wrapper = mountComponent(widget, 'something') const wrapper = mountComponent(widget, 'something')
await setTextareaValueAndTrigger(wrapper, '') await setTextareaValueAndTrigger(wrapper, '')
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith('')
expect(emitted).toBeDefined()
expect(emitted?.[0]).toContain('')
}) })
it('handles multiline text correctly', async () => { 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 wrapper = mountComponent(widget, 'single line')
const multilineText = 'Line 1\nLine 2\nLine 3' const multilineText = 'Line 1\nLine 2\nLine 3'
await setTextareaValueAndTrigger(wrapper, multilineText) await setTextareaValueAndTrigger(wrapper, multilineText)
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith(multilineText)
expect(emitted).toBeDefined()
expect(emitted?.[0]).toContain(multilineText)
}) })
it('handles special characters correctly', async () => { 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 wrapper = mountComponent(widget, 'normal')
const specialText = 'special @#$%^&*()[]{}|\\:";\'<>?,./' const specialText = 'special @#$%^&*()[]{}|\\:";\'<>?,./'
await setTextareaValueAndTrigger(wrapper, specialText) await setTextareaValueAndTrigger(wrapper, specialText)
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith(specialText)
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')
}) })
}) })
@@ -201,39 +150,36 @@ describe('WidgetTextarea Value Binding', () => {
describe('Edge Cases', () => { describe('Edge Cases', () => {
it('handles very long text', async () => { 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 wrapper = mountComponent(widget, 'short')
const longText = 'a'.repeat(10000) const longText = 'a'.repeat(10000)
await setTextareaValueAndTrigger(wrapper, longText) await setTextareaValueAndTrigger(wrapper, longText)
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith(longText)
expect(emitted).toBeDefined()
expect(emitted?.[0]).toContain(longText)
}) })
it('handles unicode characters', async () => { it('handles unicode characters', async () => {
const widget = createMockWidget('ascii') const callback = vi.fn()
const widget = createMockWidget('ascii', {}, callback)
const wrapper = mountComponent(widget, 'ascii') const wrapper = mountComponent(widget, 'ascii')
const unicodeText = '🎨 Unicode: αβγ 中文 العربية 🚀' const unicodeText = '🎨 Unicode: αβγ 中文 العربية 🚀'
await setTextareaValueAndTrigger(wrapper, unicodeText) await setTextareaValueAndTrigger(wrapper, unicodeText)
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith(unicodeText)
expect(emitted).toBeDefined()
expect(emitted?.[0]).toContain(unicodeText)
}) })
it('handles text with tabs and spaces', async () => { 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 wrapper = mountComponent(widget, 'normal')
const formattedText = '\tIndented line\n Spaced line\n\t\tDouble indent' const formattedText = '\tIndented line\n Spaced line\n\t\tDouble indent'
await setTextareaValueAndTrigger(wrapper, formattedText) await setTextareaValueAndTrigger(wrapper, formattedText)
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledExactlyOnceWith(formattedText)
expect(emitted).toBeDefined()
expect(emitted?.[0]).toContain(formattedText)
}) })
}) })
}) })

View File

@@ -16,7 +16,7 @@ describe('WidgetToggleSwitch Value Binding', () => {
callback?: (value: boolean) => void callback?: (value: boolean) => void
): SimplifiedWidget<boolean> => { ): SimplifiedWidget<boolean> => {
const valueRef = ref(value) const valueRef = ref(value)
if (callback) watch(valueRef, callback) if (callback) watch(valueRef, (v) => callback(v))
return { return {
name: 'test_toggle', name: 'test_toggle',
type: 'boolean', 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', () => { describe('Component Rendering', () => {
it('renders toggle switch component', () => { it('renders toggle switch component', () => {
const widget = createMockWidget(false) const widget = createMockWidget(false)
@@ -113,27 +71,9 @@ describe('WidgetToggleSwitch Value Binding', () => {
}) })
describe('Multiple Value Changes', () => { 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 () => { 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 wrapper = mountComponent(widget, false)
const toggle = wrapper.findComponent({ name: 'ToggleSwitch' }) const toggle = wrapper.findComponent({ name: 'ToggleSwitch' })
@@ -144,13 +84,12 @@ describe('WidgetToggleSwitch Value Binding', () => {
await toggle.setValue(true) await toggle.setValue(true)
await toggle.setValue(false) await toggle.setValue(false)
const emitted = wrapper.emitted('update:modelValue') expect(callback).toHaveBeenCalledTimes(4)
expect(emitted).toHaveLength(4)
// Verify alternating pattern // Verify alternating pattern
expect(emitted![0]).toContain(true) expect(callback).toHaveBeenNthCalledWith(1, true)
expect(emitted![1]).toContain(false) expect(callback).toHaveBeenNthCalledWith(2, false)
expect(emitted![2]).toContain(true) expect(callback).toHaveBeenNthCalledWith(3, true)
expect(emitted![3]).toContain(false) expect(callback).toHaveBeenNthCalledWith(4, false)
}) })
}) })
}) })

View File

@@ -54,7 +54,7 @@ describe('WidgetSelect Value Binding', () => {
spec?: ComboInputSpec spec?: ComboInputSpec
): SimplifiedWidget<string | undefined> => { ): SimplifiedWidget<string | undefined> => {
const valueRef = ref(value) const valueRef = ref(value)
if (callback) watch(valueRef, callback) if (callback) watch(valueRef, (v) => callback(v))
return { return {
name: 'test_select', name: 'test_select',
type: 'combo', type: 'combo',
@@ -85,67 +85,57 @@ describe('WidgetSelect Value Binding', () => {
}) })
} }
const setSelectValueAndEmit = async ( const setSelectValue = async (
wrapper: ReturnType<typeof mount>, wrapper: ReturnType<typeof mount>,
value: string value: string
) => { ) => {
const select = wrapper.findComponent({ name: 'Select' }) const select = wrapper.findComponent({ name: 'Select' })
await select.setValue(value) await select.setValue(value)
return wrapper.emitted('update:modelValue')
} }
describe('Vue Event Emission', () => { describe('Widget Value Callbacks', () => {
it('emits Vue event when selection changes', async () => { it('triggers callback when selection changes', async () => {
const widget = createMockWidget('option1') const callback = vi.fn()
const widget = createMockWidget('option1', {}, callback)
const wrapper = mountComponent(widget, 'option1') const wrapper = mountComponent(widget, 'option1')
const emitted = await setSelectValueAndEmit(wrapper, 'option2') await setSelectValue(wrapper, 'option2')
expect(emitted).toBeDefined() expect(callback).toHaveBeenCalledExactlyOnceWith('option2')
expect(emitted![0]).toContain('option2')
}) })
it('emits string value for different options', async () => { it('handles string value for different options', async () => {
const widget = createMockWidget('option1') const callback = vi.fn()
const widget = createMockWidget('option1', {}, callback)
const wrapper = mountComponent(widget, 'option1') const wrapper = mountComponent(widget, 'option1')
const emitted = await setSelectValueAndEmit(wrapper, 'option3') await setSelectValue(wrapper, 'option3')
expect(callback).toHaveBeenCalledExactlyOnceWith('option3')
expect(emitted).toBeDefined()
// Should emit the string value
expect(emitted![0]).toContain('option3')
}) })
it('handles custom option values', async () => { it('handles custom option values', async () => {
const customOptions = ['custom_a', 'custom_b', 'custom_c'] 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 wrapper = mountComponent(widget, 'custom_a')
const emitted = await setSelectValueAndEmit(wrapper, 'custom_b') await setSelectValue(wrapper, 'custom_b')
expect(emitted).toBeDefined() expect(callback).toHaveBeenCalledExactlyOnceWith('custom_b')
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 () => { 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 wrapper = mountComponent(widget, 'option1')
const emitted = await setSelectValueAndEmit(wrapper, 'option2') await setSelectValue(wrapper, 'option2')
expect(emitted).toBeDefined() expect(callback).toHaveBeenCalledExactlyOnceWith('option2')
expect(emitted![0]).toContain('option2')
}) })
}) })
@@ -176,43 +166,43 @@ describe('WidgetSelect Value Binding', () => {
'option@#$%', 'option@#$%',
'option/with\\slashes' 'option/with\\slashes'
] ]
const widget = createMockWidget(specialOptions[0], { const callback = vi.fn()
values: specialOptions const widget = createMockWidget(
}) specialOptions[0],
{
values: specialOptions
},
callback
)
const wrapper = mountComponent(widget, specialOptions[0]) const wrapper = mountComponent(widget, specialOptions[0])
const emitted = await setSelectValueAndEmit(wrapper, specialOptions[1]) await setSelectValue(wrapper, specialOptions[1])
expect(emitted).toBeDefined() expect(callback).toHaveBeenCalledExactlyOnceWith(specialOptions[1])
expect(emitted![0]).toContain(specialOptions[1])
}) })
}) })
describe('Edge Cases', () => { describe('Edge Cases', () => {
it('handles selection of non-existent option gracefully', async () => { 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 wrapper = mountComponent(widget, 'option1')
const emitted = await setSelectValueAndEmit( await setSelectValue(wrapper, 'non_existent_option')
wrapper,
'non_existent_option'
)
// Should still emit Vue event with the value // Should still trigger callback with the value
expect(emitted).toBeDefined() expect(callback).toHaveBeenCalledExactlyOnceWith('non_existent_option')
expect(emitted![0]).toContain('non_existent_option')
}) })
it('handles numeric string options correctly', async () => { it('handles numeric string options correctly', async () => {
const callback = vi.fn()
const numericOptions = ['1', '2', '10', '100'] 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 wrapper = mountComponent(widget, '1')
const emitted = await setSelectValueAndEmit(wrapper, '100') await setSelectValue(wrapper, '100')
// Should maintain string type in emitted event expect(callback).toHaveBeenCalledExactlyOnceWith('100')
expect(emitted).toBeDefined()
expect(emitted![0]).toContain('100')
}) })
}) })

View File

@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils' import type { VueWrapper } from '@vue/test-utils'
import PrimeVue from 'primevue/config' import PrimeVue from 'primevue/config'
import type { ComponentPublicInstance } from 'vue' import type { ComponentPublicInstance } from 'vue'
import { ref } from 'vue' import { nextTick, ref, watch } from 'vue'
import { describe, expect, it, vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
@@ -25,17 +25,22 @@ describe('WidgetSelectDropdown custom label mapping', () => {
values?: string[] values?: string[]
getOptionLabel?: (value: string | null) => string getOptionLabel?: (value: string | null) => string
} = {}, } = {},
spec?: ComboInputSpec spec?: ComboInputSpec,
): SimplifiedWidget<string | undefined> => ({ callback?: (value: string | undefined) => void
name: 'test_image_select', ): SimplifiedWidget<string | undefined> => {
type: 'combo', const valueRef = ref(value)
value: () => ref(value), if (callback) watch(valueRef, (v) => callback(v))
options: { return {
values: ['img_001.png', 'photo_abc.jpg', 'hash789.png'], name: 'test_image_select',
...options type: 'combo',
}, value: () => valueRef,
spec options: {
}) values: ['img_001.png', 'photo_abc.jpg', 'hash789.png'],
...options
},
spec
}
}
const mountComponent = ( const mountComponent = (
widget: SimplifiedWidget<string | undefined>, widget: SimplifiedWidget<string | undefined>,
@@ -103,26 +108,29 @@ describe('WidgetSelectDropdown custom label mapping', () => {
expect(getOptionLabel).toHaveBeenCalledWith('hash789.png') 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) => { const getOptionLabel = vi.fn((value: string | null) => {
if (!value) return 'No file' if (!value) return 'No file'
return `Custom: ${value}` return `Custom: ${value}`
}) })
const widget = createMockWidget('img_001.png', { const callback = vi.fn()
getOptionLabel const widget = createMockWidget(
}) 'img_001.png',
{
getOptionLabel
},
undefined,
callback
)
const wrapper = mountComponent(widget, 'img_001.png') const wrapper = mountComponent(widget, 'img_001.png')
// Simulate selecting an item // Simulate selecting an item
const selectedSet = new Set(['input-1']) // index 1 = photo_abc.jpg const selectedSet = new Set(['input-1']) // index 1 = photo_abc.jpg
wrapper.vm.updateSelectedItems(selectedSet) wrapper.vm.updateSelectedItems(selectedSet)
// Should emit the original value, not the custom label await nextTick()
expect(wrapper.emitted('update:modelValue')).toBeDefined() expect(callback).toHaveBeenCalledWith('photo_abc.jpg')
expect(wrapper.emitted('update:modelValue')![0]).toEqual([
'photo_abc.jpg'
])
}) })
it('falls back to original value when label mapping fails', () => { it('falls back to original value when label mapping fails', () => {