mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
232 lines
9.7 KiB
TypeScript
232 lines
9.7 KiB
TypeScript
import { describe, it, expect, test } from 'vitest'
|
|
import InputMenu, { type InputMenuProps, type InputMenuSlots } from '../../src/runtime/components/InputMenu.vue'
|
|
import ComponentRender from '../component-render'
|
|
import theme from '#build/ui/input'
|
|
import { renderForm } from '../utils/form'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import type { FormInputEvents } from '~/src/module'
|
|
import { expectEmitPayloadType } from '../utils/types'
|
|
|
|
describe('InputMenu', () => {
|
|
const sizes = Object.keys(theme.variants.size) as any
|
|
const variants = Object.keys(theme.variants.variant) as any
|
|
|
|
const items = [{
|
|
label: 'Backlog',
|
|
value: 'backlog',
|
|
icon: 'i-lucide-circle-help'
|
|
}, {
|
|
label: 'Todo',
|
|
value: 'todo',
|
|
icon: 'i-lucide-circle-plus'
|
|
}, {
|
|
label: 'In Progress',
|
|
value: 'in_progress',
|
|
icon: 'i-lucide-circle-arrow-up'
|
|
}, {
|
|
label: 'Done',
|
|
value: 'done',
|
|
icon: 'i-lucide-circle-check'
|
|
}, {
|
|
label: 'Canceled',
|
|
value: 'canceled',
|
|
icon: 'i-lucide-circle-x'
|
|
}]
|
|
|
|
const props = { open: true, portal: false, items }
|
|
|
|
it.each([
|
|
// Props
|
|
['with items', { props }],
|
|
['with modelValue', { props: { ...props, modelValue: items[0] } }],
|
|
['with defaultValue', { props: { ...props, defaultValue: items[0] } }],
|
|
['with valueKey', { props: { ...props, valueKey: 'value' } }],
|
|
['with labelKey', { props: { ...props, labelKey: 'value' } }],
|
|
['with id', { props: { ...props, id: 'id' } }],
|
|
['with name', { props: { ...props, name: 'name' } }],
|
|
['with placeholder', { props: { ...props, placeholder: 'Search...' } }],
|
|
['with disabled', { props: { ...props, disabled: true } }],
|
|
['with required', { props: { ...props, required: true } }],
|
|
['with icon', { props: { icon: 'i-lucide-search' } }],
|
|
['with leading and icon', { props: { leading: true, icon: 'i-lucide-arrow-left' } }],
|
|
['with leadingIcon', { props: { leadingIcon: 'i-lucide-arrow-left' } }],
|
|
['with trailing and icon', { props: { trailing: true, icon: 'i-lucide-arrow-right' } }],
|
|
['with trailingIcon', { props: { trailingIcon: 'i-lucide-arrow-right' } }],
|
|
['with avatar', { props: { avatar: { src: 'https://github.com/benjamincanac.png' } } }],
|
|
['with avatar and leadingIcon', { props: { avatar: { src: 'https://github.com/benjamincanac.png' }, leadingIcon: 'i-lucide-arrow-left' } }],
|
|
['with avatar and trailingIcon', { props: { avatar: { src: 'https://github.com/benjamincanac.png' }, trailingIcon: 'i-lucide-arrow-right' } }],
|
|
['with loading', { props: { loading: true } }],
|
|
['with loading and avatar', { props: { loading: true, avatar: { src: 'https://github.com/benjamincanac.png' } } }],
|
|
['with loading trailing', { props: { loading: true, trailing: true } }],
|
|
['with loading trailing and avatar', { props: { loading: true, trailing: true, avatar: { src: 'https://github.com/benjamincanac.png' } } }],
|
|
['with loadingIcon', { props: { loading: true, loadingIcon: 'i-lucide-loader' } }],
|
|
['with trailingIcon', { props: { ...props, trailingIcon: 'i-lucide-chevron-down' } }],
|
|
['with selectedIcon', { props: { ...props, selectedIcon: 'i-lucide-check' } }],
|
|
['with arrow', { props: { ...props, arrow: true } }],
|
|
...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]),
|
|
...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { ...props, variant } }]),
|
|
...variants.map((variant: string) => [`with neutral variant ${variant}`, { props: { ...props, variant, color: 'neutral' } }]),
|
|
['with ariaLabel', { props, attrs: { 'aria-label': 'Aria label' } }],
|
|
['with as', { props: { ...props, as: 'section' } }],
|
|
['with class', { props: { ...props, class: 'absolute' } }],
|
|
['with ui', { props: { ...props, ui: { group: 'p-2' } } }],
|
|
// Slots
|
|
['with leading slot', { slots: { leading: () => 'Leading slot' } }],
|
|
['with default slot', { slots: { default: () => 'Default slot' } }],
|
|
['with trailing slot', { slots: { trailing: () => 'Trailing slot' } }],
|
|
['with item slot', { props, slots: { item: () => 'Item slot' } }],
|
|
['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }],
|
|
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
|
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
|
['with create-item-label slot', { props: { ...props, searchTerm: 'New value', createItem: true }, slots: { 'create-item-label': () => 'Create item slot' } }]
|
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: InputMenuProps, slots?: Partial<InputMenuSlots> }) => {
|
|
const html = await ComponentRender(nameOrHtml, options, InputMenu)
|
|
expect(html).toMatchSnapshot()
|
|
})
|
|
|
|
describe('emits', () => {
|
|
test('update:modelValue event', async () => {
|
|
const wrapper = mount(InputMenu, { props: { items: ['Option 1', 'Option 2'] } })
|
|
const input = wrapper.findComponent({ name: 'ComboboxRoot' })
|
|
await input.setValue('Option 1')
|
|
expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [['Option 1']] })
|
|
})
|
|
|
|
test('change event', async () => {
|
|
const wrapper = mount(InputMenu, { props: { items: ['Option 1', 'Option 2'] } })
|
|
const input = wrapper.findComponent({ name: 'ComboboxRoot' })
|
|
await input.setValue('Option 1')
|
|
expect(wrapper.emitted()).toMatchObject({ change: [[{ type: 'change' }]] })
|
|
})
|
|
|
|
test('blur event', async () => {
|
|
const wrapper = mount(InputMenu, { props: { items: ['Option 1', 'Option 2'] } })
|
|
const input = wrapper.findComponent({ name: 'ComboboxRoot' })
|
|
await input.vm.$emit('update:open', false)
|
|
expect(wrapper.emitted()).toMatchObject({ blur: [[{ type: 'blur' }]] })
|
|
})
|
|
})
|
|
|
|
describe('form integration', async () => {
|
|
async function createForm(validateOn?: FormInputEvents[]) {
|
|
const wrapper = await renderForm({
|
|
props: {
|
|
validateOn,
|
|
validateOnInputDelay: 0,
|
|
async validate(state: any) {
|
|
if (state.value !== 'Option 2')
|
|
return [{ name: 'value', message: 'Error message' }]
|
|
return []
|
|
}
|
|
},
|
|
slotVars: {
|
|
items: ['Option 1', 'Option 2']
|
|
},
|
|
slotTemplate: `
|
|
<UFormField name="value">
|
|
<UInputMenu id="input" v-model="state.value" :items="items" />
|
|
</UFormField>
|
|
`
|
|
})
|
|
const input = wrapper.findComponent({ name: 'ComboboxRoot' })
|
|
return {
|
|
wrapper,
|
|
input
|
|
}
|
|
}
|
|
|
|
test('validate on blur works', async () => {
|
|
const { wrapper, input } = await createForm(['blur'])
|
|
await input.vm.$emit('update:open', false)
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('Error message')
|
|
|
|
await input.setValue('Option 2')
|
|
await input.vm.$emit('update:open', false)
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).not.toContain('Error message')
|
|
})
|
|
|
|
test('validate on change works', async () => {
|
|
const { input, wrapper } = await createForm(['change'])
|
|
|
|
input.setValue('Option 1')
|
|
await flushPromises()
|
|
expect(wrapper.text()).toContain('Error message')
|
|
|
|
input.setValue('Option 2')
|
|
await flushPromises()
|
|
expect(wrapper.text()).not.toContain('Error message')
|
|
})
|
|
|
|
test('should have the correct types', () => {
|
|
// with object item
|
|
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
|
items: [{ label: 'foo', value: 'bar' }]
|
|
})).toEqualTypeOf<[{ label: string, value: string }]>()
|
|
|
|
// with object item and multiple
|
|
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
|
items: [{ label: 'foo', value: 1 }],
|
|
multiple: true
|
|
})).toEqualTypeOf<[{ label: string, value: number }[]]>()
|
|
|
|
// with object item and valueKey
|
|
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
|
items: [{ label: 'foo', value: 'bar' }],
|
|
valueKey: 'value'
|
|
})).toEqualTypeOf<[string]>()
|
|
|
|
// with object item and multiple and valueKey
|
|
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
|
items: [{ label: 'foo', value: 1 }],
|
|
multiple: true,
|
|
valueKey: 'value'
|
|
})).toEqualTypeOf<[number[]]>()
|
|
|
|
// with object item and object valueKey
|
|
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
|
items: [{ label: 'foo', value: { id: 1, name: 'bar' } }],
|
|
valueKey: 'value'
|
|
})).toEqualTypeOf<[{ id: number, name: string }]>()
|
|
|
|
// with string item
|
|
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
|
items: ['foo']
|
|
})).toEqualTypeOf<[string]>()
|
|
|
|
// with string item and multiple
|
|
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
|
items: ['foo'],
|
|
multiple: true
|
|
})).toEqualTypeOf<[string[]]>()
|
|
|
|
// with groups
|
|
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
|
items: [['foo']]
|
|
})).toEqualTypeOf<[string]>()
|
|
|
|
// with groups and multiple
|
|
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
|
items: [['foo']],
|
|
multiple: true
|
|
})).toEqualTypeOf<[string[]]>()
|
|
|
|
// with groups, multiple and mixed types
|
|
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
|
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]],
|
|
multiple: true
|
|
})).toEqualTypeOf<[(string | number | { value: string } | { value: number })[]]>()
|
|
|
|
// with groups, multiple, mixed types and valueKey
|
|
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
|
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]],
|
|
multiple: true,
|
|
valueKey: 'value'
|
|
})).toEqualTypeOf<[(string | number)[]]>()
|
|
})
|
|
})
|
|
})
|