[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:
Christian Byrne
2025-09-14 00:20:56 -07:00
committed by GitHub
parent c7325c4da9
commit 6e2a3a0d07

View File

@@ -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')
})
})
})