mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 10:00:08 +00:00
[test] Add component test for SelectButton Vue widget (#5530)
* add component test for select button * [refactor] improve test structure and typing - addresses @DrJKL review comments - Use proper SimplifiedWidget['options'] type instead of loose object type - Extract helper functions as module-level function declarations for better organization - Remove type assertion violation by using proper union type for null/undefined values - Format code with prettier to maintain consistency * [refactor] use safer optional chaining in test assertions - addresses @DrJKL's safety preference Replace emitted\![0] with emitted?.[0] for safer array access in test expectations. This follows the same pattern as applied to the textarea widget tests for consistency.
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import WidgetSelectButton from './WidgetSelectButton.vue'
|
||||
|
||||
function createMockWidget(
|
||||
value: string = 'option1',
|
||||
options: SimplifiedWidget['options'] = {},
|
||||
callback?: (value: string) => void
|
||||
): SimplifiedWidget<string> {
|
||||
return {
|
||||
name: 'test_selectbutton',
|
||||
type: 'string',
|
||||
value,
|
||||
options,
|
||||
callback
|
||||
}
|
||||
}
|
||||
|
||||
function mountComponent(
|
||||
widget: SimplifiedWidget<string>,
|
||||
modelValue: string,
|
||||
readonly = false
|
||||
) {
|
||||
return mount(WidgetSelectButton, {
|
||||
global: {
|
||||
plugins: [PrimeVue]
|
||||
},
|
||||
props: {
|
||||
widget,
|
||||
modelValue,
|
||||
readonly
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function clickSelectButton(
|
||||
wrapper: ReturnType<typeof mount>,
|
||||
optionText: string
|
||||
) {
|
||||
const buttons = wrapper.findAll('button')
|
||||
const targetButton = buttons.find((button) =>
|
||||
button.text().includes(optionText)
|
||||
)
|
||||
|
||||
if (!targetButton) {
|
||||
throw new Error(`Button with text "${optionText}" not found`)
|
||||
}
|
||||
|
||||
await targetButton.trigger('click')
|
||||
return targetButton
|
||||
}
|
||||
|
||||
describe('WidgetSelectButton Button Selection', () => {
|
||||
describe('Basic Rendering', () => {
|
||||
it('renders FormSelectButton component', () => {
|
||||
const widget = createMockWidget('option1', {
|
||||
values: ['option1', 'option2', 'option3']
|
||||
})
|
||||
const wrapper = mountComponent(widget, 'option1')
|
||||
|
||||
const formSelectButton = wrapper.findComponent({
|
||||
name: 'FormSelectButton'
|
||||
})
|
||||
expect(formSelectButton.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders buttons for each option', () => {
|
||||
const options = ['first', 'second', 'third']
|
||||
const widget = createMockWidget('first', { values: options })
|
||||
const wrapper = mountComponent(widget, 'first')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(3)
|
||||
expect(buttons[0].text()).toBe('first')
|
||||
expect(buttons[1].text()).toBe('second')
|
||||
expect(buttons[2].text()).toBe('third')
|
||||
})
|
||||
|
||||
it('handles empty options array', () => {
|
||||
const widget = createMockWidget('', { values: [] })
|
||||
const wrapper = mountComponent(widget, '')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles missing values option', () => {
|
||||
const widget = createMockWidget('')
|
||||
const wrapper = mountComponent(widget, '')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Selection State', () => {
|
||||
it('highlights selected option', () => {
|
||||
const options = ['apple', 'banana', 'cherry']
|
||||
const widget = createMockWidget('banana', { values: options })
|
||||
const wrapper = mountComponent(widget, 'banana')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
const selectedButton = buttons[1] // 'banana'
|
||||
const unselectedButton = buttons[0] // 'apple'
|
||||
|
||||
expect(selectedButton.classes()).toContain('bg-white')
|
||||
expect(selectedButton.classes()).toContain('text-neutral-900')
|
||||
expect(unselectedButton.classes()).not.toContain('bg-white')
|
||||
expect(unselectedButton.classes()).not.toContain('text-neutral-900')
|
||||
})
|
||||
|
||||
it('handles no selection gracefully', () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('nonexistent', { values: options })
|
||||
const wrapper = mountComponent(widget, 'nonexistent')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
buttons.forEach((button) => {
|
||||
expect(button.classes()).not.toContain('bg-white')
|
||||
expect(button.classes()).not.toContain('text-neutral-900')
|
||||
})
|
||||
})
|
||||
|
||||
it('updates selection when modelValue changes', async () => {
|
||||
const options = ['first', 'second', 'third']
|
||||
const widget = createMockWidget('first', { values: options })
|
||||
const wrapper = mountComponent(widget, 'first')
|
||||
|
||||
// Initially 'first' is selected
|
||||
let buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].classes()).toContain('bg-white')
|
||||
|
||||
// Update to 'second'
|
||||
await wrapper.setProps({ modelValue: 'second' })
|
||||
buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].classes()).not.toContain('bg-white')
|
||||
expect(buttons[1].classes()).toContain('bg-white')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('emits update:modelValue when button is clicked', async () => {
|
||||
const options = ['first', 'second', 'third']
|
||||
const widget = createMockWidget('first', { values: options })
|
||||
const wrapper = mountComponent(widget, 'first')
|
||||
|
||||
await clickSelectButton(wrapper, 'second')
|
||||
|
||||
const emitted = wrapper.emitted('update:modelValue')
|
||||
expect(emitted).toBeDefined()
|
||||
expect(emitted?.[0]).toEqual(['second'])
|
||||
})
|
||||
|
||||
it('handles callback execution when provided', async () => {
|
||||
const mockCallback = vi.fn()
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget(
|
||||
'option1',
|
||||
{ values: options },
|
||||
mockCallback
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'option1')
|
||||
|
||||
await clickSelectButton(wrapper, 'option2')
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('option2')
|
||||
})
|
||||
|
||||
it('handles missing callback gracefully', async () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('option1', { values: options }, undefined)
|
||||
const wrapper = mountComponent(widget, 'option1')
|
||||
|
||||
await clickSelectButton(wrapper, 'option2')
|
||||
|
||||
// Should still emit Vue event
|
||||
const emitted = wrapper.emitted('update:modelValue')
|
||||
expect(emitted).toBeDefined()
|
||||
expect(emitted?.[0]).toEqual(['option2'])
|
||||
})
|
||||
|
||||
it('allows clicking same option again', async () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('option1', { values: options })
|
||||
const wrapper = mountComponent(widget, 'option1')
|
||||
|
||||
await clickSelectButton(wrapper, 'option1')
|
||||
|
||||
const emitted = wrapper.emitted('update:modelValue')
|
||||
expect(emitted).toBeDefined()
|
||||
expect(emitted?.[0]).toEqual(['option1'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Readonly Mode', () => {
|
||||
it('disables all buttons when readonly', () => {
|
||||
const options = ['option1', 'option2', 'option3']
|
||||
const widget = createMockWidget('option1', { values: options })
|
||||
const wrapper = mountComponent(widget, 'option1', true)
|
||||
|
||||
const formSelectButton = wrapper.findComponent({
|
||||
name: 'FormSelectButton'
|
||||
})
|
||||
expect(formSelectButton.props('disabled')).toBe(true)
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
buttons.forEach((button) => {
|
||||
expect(button.element.disabled).toBe(true)
|
||||
expect(button.classes()).toContain('cursor-not-allowed')
|
||||
expect(button.classes()).toContain('opacity-50')
|
||||
})
|
||||
})
|
||||
|
||||
it('does not emit changes in readonly mode', async () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('option1', { values: options })
|
||||
const wrapper = mountComponent(widget, 'option1', true)
|
||||
|
||||
await clickSelectButton(wrapper, 'option2')
|
||||
|
||||
const emitted = wrapper.emitted('update:modelValue')
|
||||
expect(emitted).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not change visual state in readonly mode', () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('option1', { values: options })
|
||||
const wrapper = mountComponent(widget, 'option1', true)
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
buttons.forEach((button) => {
|
||||
expect(button.classes()).not.toContain('hover:bg-zinc-200/50')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Option Types', () => {
|
||||
it('handles string options', () => {
|
||||
const options = ['apple', 'banana', 'cherry']
|
||||
const widget = createMockWidget('banana', { values: options })
|
||||
const wrapper = mountComponent(widget, 'banana')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].text()).toBe('apple')
|
||||
expect(buttons[1].text()).toBe('banana')
|
||||
expect(buttons[2].text()).toBe('cherry')
|
||||
})
|
||||
|
||||
it('handles number options', () => {
|
||||
const options = [1, 2, 3]
|
||||
const widget = createMockWidget('2', { values: options })
|
||||
const wrapper = mountComponent(widget, '2')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].text()).toBe('1')
|
||||
expect(buttons[1].text()).toBe('2')
|
||||
expect(buttons[2].text()).toBe('3')
|
||||
|
||||
// The selected button should be the one with '2'
|
||||
expect(buttons[1].classes()).toContain('bg-white')
|
||||
})
|
||||
|
||||
it('handles object options with label and value', () => {
|
||||
const options = [
|
||||
{ label: 'First Option', value: 'first' },
|
||||
{ label: 'Second Option', value: 'second' },
|
||||
{ label: 'Third Option', value: 'third' }
|
||||
]
|
||||
const widget = createMockWidget('second', { values: options })
|
||||
const wrapper = mountComponent(widget, 'second')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].text()).toBe('First Option')
|
||||
expect(buttons[1].text()).toBe('Second Option')
|
||||
expect(buttons[2].text()).toBe('Third Option')
|
||||
|
||||
// 'second' should be selected
|
||||
expect(buttons[1].classes()).toContain('bg-white')
|
||||
})
|
||||
|
||||
it('emits correct values for object options', async () => {
|
||||
const options = [
|
||||
{ label: 'First', value: 'first_val' },
|
||||
{ label: 'Second', value: 'second_val' }
|
||||
]
|
||||
const widget = createMockWidget('first_val', { values: options })
|
||||
const wrapper = mountComponent(widget, 'first_val')
|
||||
|
||||
await clickSelectButton(wrapper, 'Second')
|
||||
|
||||
const emitted = wrapper.emitted('update:modelValue')
|
||||
expect(emitted).toBeDefined()
|
||||
expect(emitted?.[0]).toEqual(['second_val'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles options with special characters', () => {
|
||||
const options = ['@#$%^&*()', '{}[]|\\:";\'<>?,./']
|
||||
const widget = createMockWidget(options[0], { values: options })
|
||||
const wrapper = mountComponent(widget, options[0])
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].text()).toBe('@#$%^&*()')
|
||||
expect(buttons[1].text()).toBe('{}[]|\\:";\'<>?,./')
|
||||
})
|
||||
|
||||
it('handles empty string options', () => {
|
||||
const options = ['', 'not empty', ' ', 'normal']
|
||||
const widget = createMockWidget('', { values: options })
|
||||
const wrapper = mountComponent(widget, '')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(4)
|
||||
expect(buttons[0].classes()).toContain('bg-white') // Empty string is selected
|
||||
})
|
||||
|
||||
it('handles null/undefined in options', () => {
|
||||
const options: (string | null | undefined)[] = [
|
||||
'valid',
|
||||
null,
|
||||
undefined,
|
||||
'another'
|
||||
]
|
||||
const widget = createMockWidget('valid', { values: options })
|
||||
const wrapper = mountComponent(widget, 'valid')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(4)
|
||||
expect(buttons[0].classes()).toContain('bg-white')
|
||||
})
|
||||
|
||||
it('handles very long option text', () => {
|
||||
const longText =
|
||||
'This is a very long option text that might cause layout issues if not handled properly'
|
||||
const options = ['short', longText, 'normal']
|
||||
const widget = createMockWidget('short', { values: options })
|
||||
const wrapper = mountComponent(widget, 'short')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[1].text()).toBe(longText)
|
||||
})
|
||||
|
||||
it('handles large number of options', () => {
|
||||
const options = Array.from({ length: 20 }, (_, i) => `option${i + 1}`)
|
||||
const widget = createMockWidget('option5', { values: options })
|
||||
const wrapper = mountComponent(widget, 'option5')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(20)
|
||||
expect(buttons[4].classes()).toContain('bg-white') // option5 is at index 4
|
||||
})
|
||||
|
||||
it('handles duplicate options', () => {
|
||||
const options = ['duplicate', 'unique', 'duplicate', 'unique']
|
||||
const widget = createMockWidget('duplicate', { values: options })
|
||||
const wrapper = mountComponent(widget, 'duplicate')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(4)
|
||||
// Both 'duplicate' buttons should be highlighted (due to value matching)
|
||||
expect(buttons[0].classes()).toContain('bg-white')
|
||||
expect(buttons[2].classes()).toContain('bg-white')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Styling and Layout', () => {
|
||||
it('applies proper button styling', () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('option1', { values: options })
|
||||
const wrapper = mountComponent(widget, 'option1')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
buttons.forEach((button) => {
|
||||
expect(button.classes()).toContain('flex-1')
|
||||
expect(button.classes()).toContain('h-6')
|
||||
expect(button.classes()).toContain('px-5')
|
||||
expect(button.classes()).toContain('rounded')
|
||||
expect(button.classes()).toContain('text-center')
|
||||
expect(button.classes()).toContain('text-xs')
|
||||
})
|
||||
})
|
||||
|
||||
it('applies container styling', () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('option1', { values: options })
|
||||
const wrapper = mountComponent(widget, 'option1')
|
||||
|
||||
const container = wrapper.find('div').element
|
||||
expect(container.className).toContain('p-1')
|
||||
expect(container.className).toContain('inline-flex')
|
||||
expect(container.className).toContain('justify-center')
|
||||
expect(container.className).toContain('items-center')
|
||||
expect(container.className).toContain('gap-1')
|
||||
})
|
||||
|
||||
it('applies hover effects for non-selected options', () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('option1', { values: options })
|
||||
const wrapper = mountComponent(widget, 'option1', false)
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
const unselectedButton = buttons[1] // 'option2'
|
||||
|
||||
expect(unselectedButton.classes()).toContain('hover:bg-zinc-200/50')
|
||||
expect(unselectedButton.classes()).toContain('cursor-pointer')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration with Layout', () => {
|
||||
it('renders within WidgetLayoutField', () => {
|
||||
const widget = createMockWidget('test', { values: ['test'] })
|
||||
const wrapper = mountComponent(widget, 'test')
|
||||
|
||||
const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
|
||||
expect(layoutField.exists()).toBe(true)
|
||||
expect(layoutField.props('widget')).toEqual(widget)
|
||||
})
|
||||
|
||||
it('passes widget name to layout field', () => {
|
||||
const widget = createMockWidget('test', { values: ['test'] })
|
||||
widget.name = 'custom_select_button'
|
||||
const wrapper = mountComponent(widget, 'test')
|
||||
|
||||
const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
|
||||
expect(layoutField.props('widget').name).toBe('custom_select_button')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user