import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { VList } from 'vuetify/components'
import { nextTick } from 'vue'
import SySelect from '../SySelect.vue'
type ItemType = {
[key: string]: unknown
}
describe('SySelect.vue', () => {
it('renders the component with default props', () => {
const wrapper = mount(SySelect, {
attachTo: document.body,
})
expect(wrapper.exists()).toBe(true)
expect(wrapper.find('.sy-select').exists()).toBe(true)
wrapper.unmount()
})
it('renders prepend and append slots content when provided', () => {
const wrapper = mount(SySelect, {
props: {
items: [{ text: 'Option 1', value: '1' }],
},
slots: {
prepend: 'prepend',
append: 'append',
},
attachTo: document.body,
})
expect(wrapper.find('.custom-prepend').exists()).toBe(true)
expect(wrapper.find('.custom-append').exists()).toBe(true)
wrapper.unmount()
})
it('renders tooltip icons when prependTooltip/appendTooltip are provided', () => {
const wrapper = mount(SySelect, {
props: {
items: [{ text: 'Option 1', value: '1' }],
label: 'Test',
prependTooltip: 'Information à gauche du champ',
appendTooltip: 'Information à droite du champ',
tooltipLocation: 'top',
},
attachTo: document.body,
})
// SyIcon renders a v-icon with aria-label
expect(wrapper.findAll('[aria-label="Test - info"]').length).toBe(2)
wrapper.unmount()
})
it('emits prepend-icon-click and does not open menu when clicking prepend icon', async () => {
const wrapper = mount(SySelect, {
props: {
items: [{ text: 'Option 1', value: '1' }],
label: 'Test',
prependIcon: 'success',
disableClickButton: false,
},
attachTo: document.body,
})
expect(wrapper.findComponent(VList).exists()).toBe(false)
await wrapper.find('[aria-label="Test - bouton success"]').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.emitted()['prepend-icon-click']).toBeTruthy()
expect(wrapper.findComponent(VList).exists()).toBe(false)
wrapper.unmount()
})
it('emits append-icon-click and does not open menu when clicking append icon', async () => {
const wrapper = mount(SySelect, {
props: {
items: [{ text: 'Option 1', value: '1' }],
label: 'Test',
appendIcon: 'success',
disableClickButton: false,
},
attachTo: document.body,
})
expect(wrapper.findComponent(VList).exists()).toBe(false)
await wrapper.find('[aria-label="Test - bouton success"]').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.emitted()['append-icon-click']).toBeTruthy()
expect(wrapper.findComponent(VList).exists()).toBe(false)
wrapper.unmount()
})
it('displays the selected item text', async () => {
const items = [{ text: 'Option 1', value: '1' }, { text: 'Option 2', value: '2' }]
const wrapper = mount(SySelect, {
props: { items, modelValue: { text: 'Option 1', value: '1' } },
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
const firstItem = wrapper
.findComponent(VList)
.findAll('.v-list-item').at(0)
await firstItem!.trigger('click')
expect(wrapper.find('input').element.value).toBe('Option 1')
wrapper.unmount()
})
it('closes the menu on escape key press', async () => {
const items = [{ text: 'Option 1', value: '1' }, { text: 'Option 2', value: '2' }]
const wrapper = mount(SySelect, {
props: { items },
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
await wrapper
.findComponent(VList)
.find('.v-list').trigger('keydown.esc')
expect(wrapper.find('.v-list').exists()).toBe(false)
wrapper.unmount()
})
it('renders error messages when provided', () => {
const errorMessages = ['Error 1']
const wrapper = mount(SySelect, {
props: { errorMessages, hideDetails: false },
attachTo: document.body,
})
const message = wrapper.find('.v-messages__message')
expect(message.exists()).toBe(true)
expect(message.text()).toContain('Error 1')
wrapper.unmount()
})
describe('hideDetails', () => {
it('masque les messages de validation quand hideDetails est true', async () => {
const wrapper = mount(SySelect, {
props: {
errorMessages: ['Erreur de test'],
hideDetails: true,
},
attachTo: document.body,
})
expect(wrapper.find('.v-messages__message').exists()).toBe(false)
wrapper.unmount()
})
it('affiche les messages de validation quand hideDetails est false', async () => {
const wrapper = mount(SySelect, {
props: {
errorMessages: ['Erreur de test'],
hideDetails: false,
},
attachTo: document.body,
})
const message = wrapper.find('.v-messages__message')
expect(message.exists()).toBe(true)
expect(message.text()).toContain('Erreur de test')
wrapper.unmount()
})
it('affiche la zone de messages par défaut (hideDetails vaut false par défaut)', () => {
const wrapper = mount(SySelect, {
attachTo: document.body,
})
expect(wrapper.find('.v-messages').exists()).toBe(true)
wrapper.unmount()
})
it('n\'affiche pas le helpText en dessous du champ quand hideDetails est true et qu\'il y a des erreurs', () => {
const wrapper = mount(SySelect, {
props: {
helpText: 'Texte d\'aide',
errorMessages: ['Erreur de test'],
hideDetails: true,
},
attachTo: document.body,
})
expect(wrapper.find('.help-text-below').exists()).toBe(false)
wrapper.unmount()
})
it('affiche le helpText en dessous du champ quand hideDetails est false et qu\'il y a des erreurs', () => {
const wrapper = mount(SySelect, {
props: {
helpText: 'Texte d\'aide',
errorMessages: ['Erreur de test'],
hideDetails: false,
},
attachTo: document.body,
})
expect(wrapper.find('.help-text-below').exists()).toBe(true)
expect(wrapper.find('.help-text-below').text()).toContain('Texte d\'aide')
wrapper.unmount()
})
})
it('keeps the label active when a validation error is displayed', async () => {
const wrapper = mount(SySelect, {
props: {
items: [{ text: 'Option 1', value: '1' }],
label: 'Option',
required: true,
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exposed method is not part of the inferred public instance type
const isValid = await (wrapper.vm as any).validateOnSubmit()
await nextTick()
expect(isValid).toBe(false)
expect(wrapper.find('.v-field').classes()).toContain('v-field--active')
wrapper.unmount()
})
it('does not render error messages when not provided', () => {
const wrapper = mount(SySelect, {
attachTo: document.body,
})
expect(wrapper.find('.v-messages__message').exists()).toBe(false)
wrapper.unmount()
})
it('returns the correct item text using getItemText', () => {
const wrapper = mount(SySelect, {
props: { textKey: 'text' },
attachTo: document.body,
})
const item = { text: 'Option 1', value: '1' }
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.getItemText(item)).toBe('Option 1')
wrapper.unmount()
})
it('returns default text when selectedItem is null', () => {
const wrapper = mount(SySelect, {
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.selectedItemText).toBe('')
wrapper.unmount()
})
it('returns the correct text when selectedItem is an object', async () => {
const wrapper = mount(SySelect, {
props: {
modelValue: { text: 'Option 1', value: '1' },
textKey: 'text',
returnObject: true,
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
await wrapper.setProps({ modelValue: { text: 'Option 1', value: '1' } })
expect(instance.selectedItemText).toBe('Option 1')
wrapper.unmount()
})
it('returns the correct text when selectedItem is a value', async () => {
const wrapper = mount(SySelect, {
props: {
items: [{ text: 'Option 1', value: '1' }, { text: 'Option 2', value: '2' }],
modelValue: '1',
textKey: 'text',
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
await wrapper.setProps({ modelValue: '2' })
await wrapper.vm.$nextTick()
expect(instance.selectedItemText).toBe('Option 2')
wrapper.unmount()
})
it('formats items correctly', () => {
const items = ['Option 1', 'Option 2'] as unknown as ItemType[]
const wrapper = mount(SySelect, {
props: { items, textKey: 'text', valueKey: 'value' },
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const formattedItems = (wrapper.vm as any).formattedItems
expect(formattedItems).toEqual([
{ text: 'Option 1', value: 'Option 1' },
{ text: 'Option 2', value: 'Option 2' },
])
wrapper.unmount()
})
it('applies the correct button class when outlined is true', () => {
const wrapper = mount(SySelect, {
props: { outlined: true },
attachTo: document.body,
})
expect(wrapper.find('.v-field--variant-outlined').exists()).toBe(true)
wrapper.unmount()
})
it('does not apply the outlined button class when outlined is false', () => {
const wrapper = mount(SySelect, {
props: { outlined: false },
attachTo: document.body,
})
expect(wrapper.find('.sy-select').classes()).not.toContain('v-btn--variant-outlined')
wrapper.unmount()
})
it('updates selectedItem when v-model changes', async () => {
const wrapper = mount(SySelect, {
props: { modelValue: { text: 'Option 1', value: '1' }, textKey: 'text' },
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.selectedItem).toEqual({ text: 'Option 1', value: '1' })
await wrapper.setProps({ modelValue: { text: 'Option 2', value: '2' } })
expect(instance.selectedItem).toEqual({ text: 'Option 2', value: '2' })
wrapper.unmount()
})
it('emits update:modelValue when selectedItem changes', async () => {
const wrapper = mount(SySelect, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
props: { modelValue: null as any, textKey: 'text' },
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
instance.selectItem({ text: 'Option 1', value: '1' })
await wrapper.vm.$nextTick()
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual(['1'])
wrapper.unmount()
})
it('ferme le menu avec la méthode closeList', async () => {
const wrapper = mount(SySelect, {
props: {
items: [{ text: 'Option 1', value: '1' }],
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.isOpen).toBe(true)
instance.closeList()
await wrapper.vm.$nextTick()
expect(instance.isOpen).toBe(false)
wrapper.unmount()
})
describe('Affichage de l\'astérisque', () => {
it('affiche l\'astérisque quand displayAsterisk et required sont true', () => {
const wrapper = mount(SySelect, {
props: {
displayAsterisk: true,
required: true,
label: 'Test Label',
},
attachTo: document.body,
})
const html = wrapper.html()
expect(html).toContain('Test Label *')
wrapper.unmount()
})
it('n\'affiche pas l\'astérisque quand displayAsterisk est false', () => {
const wrapper = mount(SySelect, {
props: {
displayAsterisk: false,
required: true,
label: 'Test Label',
},
attachTo: document.body,
})
const html = wrapper.html()
expect(html).not.toContain('Test Label *')
expect(html).toContain('Test Label')
wrapper.unmount()
})
it('n\'affiche pas l\'astérisque quand required est false', () => {
const wrapper = mount(SySelect, {
props: {
displayAsterisk: true,
required: false,
label: 'Test Label',
},
attachTo: document.body,
})
const html = wrapper.html()
expect(html).not.toContain('Test Label *')
expect(html).toContain('Test Label')
wrapper.unmount()
})
})
describe('Mode readonly', () => {
it('empêche l\'ouverture du menu en mode readonly', async () => {
const wrapper = mount(SySelect, {
props: {
readonly: true,
items: [{ text: 'Option 1', value: '1' }],
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.find('.v-list').exists()).toBe(false)
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.isOpen).toBe(false)
wrapper.unmount()
})
it('affiche correctement le champ en mode readonly', () => {
const wrapper = mount(SySelect, {
props: {
readonly: true,
modelValue: { text: 'Option 1', value: '1' },
textKey: 'text',
returnObject: true,
},
attachTo: document.body,
})
expect(wrapper.find('.v-input--readonly').exists()).toBe(true)
expect(wrapper.html()).toContain('Option 1')
wrapper.unmount()
})
})
describe('Option clearable', () => {
it('affiche l\'icône de suppression quand clearable est true et qu\'une valeur est sélectionnée', async () => {
const wrapper = mount(SySelect, {
props: {
clearable: true,
modelValue: { text: 'Option 1', value: '1' },
returnObject: true,
},
attachTo: document.body,
})
expect(wrapper.find('.sy-select__clear-icon').exists()).toBe(true)
wrapper.unmount()
})
it('n\'affiche pas l\'icône de suppression quand clearable est false', () => {
const wrapper = mount(SySelect, {
props: {
clearable: false,
modelValue: { text: 'Option 1', value: '1' },
returnObject: true,
},
attachTo: document.body,
})
expect(wrapper.find('.v-icon.mdi-close-circle').exists()).toBe(false)
wrapper.unmount()
})
it('efface la valeur sélectionnée avec la méthode selectItem', async () => {
const wrapper = mount(SySelect, {
props: {
clearable: true,
modelValue: { text: 'Option 1', value: '1' },
returnObject: true,
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
instance.selectItem(null)
await wrapper.vm.$nextTick()
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual([null])
wrapper.unmount()
})
})
describe('Validation', () => {
it('affiche une erreur pour un champ requis sans valeur', async () => {
const wrapper = mount(SySelect, {
props: {
required: true,
label: 'Test Label',
modelValue: undefined,
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasError).toBe(true)
wrapper.unmount()
})
it('n\'affiche pas d\'erreur pour un champ requis avec une valeur', async () => {
const wrapper = mount(SySelect, {
props: {
required: true,
label: 'Test Label',
modelValue: { text: 'Option 1', value: '1' },
returnObject: true,
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasError).toBe(false)
wrapper.unmount()
})
it('n\'affiche pas d\'erreur à l\'ouverture du menu mais seulement à la fermeture', async () => {
const wrapper = mount(SySelect, {
props: {
required: true,
label: 'Test Label',
modelValue: undefined,
items: [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
],
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
// Au départ, pas d'erreur
expect(instance.hasError).toBe(false)
// Ouverture du menu - l'erreur ne doit pas s'afficher
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
expect(instance.hasError).toBe(false)
expect(instance.isOpen).toBe(true)
// Fermeture du menu sans sélection - l'erreur doit s'afficher
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
expect(instance.hasError).toBe(true)
expect(instance.isOpen).toBe(false)
wrapper.unmount()
})
describe('disableErrorHandling', () => {
it('n\'affiche pas d\'erreur pour un champ requis sans valeur quand disableErrorHandling est true', async () => {
const wrapper = mount(SySelect, {
props: {
required: true,
label: 'Test Label',
modelValue: undefined,
disableErrorHandling: true,
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasError).toBe(false)
wrapper.unmount()
})
it('ignore les errorMessages quand disableErrorHandling est true', () => {
const wrapper = mount(SySelect, {
props: {
errorMessages: ['Erreur forcée'],
disableErrorHandling: true,
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasError).toBe(false)
expect(wrapper.find('.v-messages__message').exists()).toBe(false)
wrapper.unmount()
})
it('n\'évalue pas les customRules quand disableErrorHandling est true', async () => {
const wrapper = mount(SySelect, {
props: {
modelValue: '1',
disableErrorHandling: true,
isValidateOnBlur: false,
customRules: [{
type: 'custom',
options: {
validate: () => false,
message: 'Toujours invalide',
},
}],
},
attachTo: document.body,
})
await wrapper.vm.$nextTick()
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasError).toBe(false)
expect(wrapper.find('.v-messages__message').exists()).toBe(false)
wrapper.unmount()
})
it('affiche les erreurs normalement quand disableErrorHandling est false', async () => {
const wrapper = mount(SySelect, {
props: {
errorMessages: ['Erreur visible'],
disableErrorHandling: false,
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasError).toBe(true)
expect(wrapper.find('.v-messages__message').text()).toContain('Erreur visible')
wrapper.unmount()
})
})
it('valide immédiatement quand isValidateOnBlur est false', async () => {
const wrapper = mount(SySelect, {
props: {
items: [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
],
label: 'Test Label',
modelValue: undefined,
isValidateOnBlur: false,
customRules: [{
type: 'custom',
options: {
validate: (value: unknown) => value === '2',
message: 'Test error message',
},
}],
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasError).toBe(false)
// Sélection de Option 1 via interaction utilisateur
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.findComponent(VList).findAll('.v-list-item').at(0)!.trigger('click')
await wrapper.setProps({ modelValue: '1' })
await vi.waitUntil(() => instance.hasError === true)
expect(wrapper.find('.v-messages').text()).toContain('Test error message')
// Sélection de Option 2 via interaction utilisateur
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.findComponent(VList).findAll('.v-list-item').at(1)!.trigger('click')
await wrapper.setProps({ modelValue: '2' })
await vi.waitUntil(() => instance.hasError === false)
expect(wrapper.find('.v-messages').text()).not.toContain('Test error message')
wrapper.unmount()
})
it('masque le message de succes mais conserve l\'etat de succes quand showSuccessMessages est false', async () => {
const wrapper = mount(SySelect, {
props: {
items: [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
],
label: 'Test Label',
modelValue: undefined,
isValidateOnBlur: false,
showSuccessMessages: false,
customSuccessRules: [{
type: 'custom',
options: {
validate: (value: unknown) => value === '2',
successMessage: 'Test success message',
},
}],
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasSuccess).toBe(false)
await wrapper.setProps({ modelValue: '2' })
await vi.waitUntil(() => instance.hasSuccess === true)
expect(wrapper.find('.success-field').exists()).toBe(true)
expect(wrapper.findAll('.v-messages__message')).toHaveLength(0)
expect(wrapper.text()).not.toContain('Test success message')
wrapper.unmount()
})
it('ne valide pas lors d\'un changement de valeur mais seulement à la fermeture du menu quand isValidateOnBlur est true', async () => {
const wrapper = mount(SySelect, {
props: {
items: [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
],
label: 'Test Label',
modelValue: undefined,
isValidateOnBlur: true,
customRules: [{
type: 'custom',
options: {
validate: (value: unknown) => value === '2',
message: 'Test error message',
},
}],
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasError).toBe(false)
// Changement de valeur programmatique — l'erreur ne doit PAS apparaître
await wrapper.setProps({ modelValue: '1' })
await wrapper.vm.$nextTick()
expect(instance.hasError).toBe(false)
// Ouverture puis fermeture du menu (= blur) — l'erreur doit apparaître
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
await vi.waitUntil(() => instance.hasError === true)
expect(wrapper.find('.v-messages').text()).toContain('Test error message')
wrapper.unmount()
})
it('déclenche la validation au blur natif sans ouvrir le menu', async () => {
const wrapper = mount(SySelect, {
props: {
required: true,
label: 'Test Label',
modelValue: undefined,
items: [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
],
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasError).toBe(false)
// Simuler un blur natif sur l'input sans jamais ouvrir le menu
const input = wrapper.find('input')
await input.trigger('blur')
await wrapper.vm.$nextTick()
await vi.waitUntil(() => instance.hasError === true)
expect(wrapper.find('.v-messages').text()).toContain('requis')
wrapper.unmount()
})
it('affiche un avertissement avec customWarningRules', async () => {
const wrapper = mount(SySelect, {
props: {
items: [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
],
label: 'Test Label',
modelValue: undefined,
isValidateOnBlur: false,
customWarningRules: [{
type: 'custom',
options: {
validate: (value: unknown) => value === '2',
warningMessage: 'Option 1 est dépréciée.',
},
}],
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasWarning).toBe(false)
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.findComponent(VList).findAll('.v-list-item').at(0)!.trigger('click')
await wrapper.setProps({ modelValue: '1' })
await vi.waitUntil(() => instance.hasWarning === true)
expect(wrapper.find('.v-messages').text()).toContain('Option 1 est dépréciée.')
wrapper.unmount()
})
it('validateOnSubmit retourne true quand le champ est valide', async () => {
const wrapper = mount(SySelect, {
props: {
required: true,
label: 'Test Label',
modelValue: '1',
items: [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
],
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const isValid = await (wrapper.vm as any).validateOnSubmit()
await nextTick()
expect(isValid).toBe(true)
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
expect((wrapper.vm as any).hasError).toBe(false)
wrapper.unmount()
})
it('affiche le message de succès avec customSuccessRules quand showSuccessMessages est true', async () => {
const wrapper = mount(SySelect, {
props: {
items: [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
],
label: 'Test Label',
modelValue: undefined,
isValidateOnBlur: false,
showSuccessMessages: true,
customSuccessRules: [{
type: 'custom',
options: {
validate: (value: unknown) => value === '2',
successMessage: 'Test success message',
},
}],
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.hasSuccess).toBe(false)
await wrapper.setProps({ modelValue: '2' })
await vi.waitUntil(() => instance.hasSuccess === true)
expect(wrapper.find('.success-field').exists()).toBe(true)
expect(wrapper.find('.v-messages').text()).toContain('Test success message')
wrapper.unmount()
})
it('clearValidation remet l\'état d\'erreur à zéro', async () => {
const wrapper = mount(SySelect, {
props: {
required: true,
label: 'Test Label',
modelValue: undefined,
items: [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
],
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
await instance.validateOnSubmit()
await nextTick()
expect(instance.hasError).toBe(true)
instance.clearValidation()
await nextTick()
expect(instance.hasError).toBe(false)
expect(wrapper.find('.v-messages__message').exists()).toBe(false)
wrapper.unmount()
})
})
describe('Comportement du menu', () => {
it('ouvre et ferme le menu au clic', async () => {
const wrapper = mount(SySelect, {
props: {
items: [{ text: 'Option 1', value: '1' }],
},
attachTo: document.body,
})
function findList() {
return wrapper.findComponent(VList)
}
expect(findList().exists()).toBe(false)
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
expect(findList().exists()).toBe(true)
wrapper.unmount()
})
it('ouvre et ferme le menu au clic2', async () => {
const wrapper = mount(SySelect, {
props: {
items: [{ text: 'Option 1', value: '1' }],
},
attachTo: document.body,
})
expect(wrapper.findComponent(VList).exists()).toBe(false)
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.findComponent(VList).exists()).toBe(true)
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.vm.isOpen).toBe(false)
wrapper.unmount()
})
it('met à jour isOpen quand on ouvre le menu', async () => {
const wrapper = mount(SySelect, {
props: {
items: [{ text: 'Option 1', value: '1' }],
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
expect(instance.isOpen).toBe(false)
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
expect(instance.isOpen).toBe(true)
wrapper.unmount()
})
})
it('ferme le menu après un clic sur le sélecteur', async () => {
const wrapper = mount(SySelect, {
props: {
items: [{ text: 'Option 1', value: '1' }],
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper
.findComponent(VList)
.find('.v-list').exists()).toBe(true)
await wrapper.find('.sy-select').trigger('mouseleave')
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.vm.isOpen).toBe(false)
wrapper.unmount()
})
it('use closeList method', async () => {
const wrapper = mount(SySelect, {
attachTo: document.body,
})
wrapper.vm.closeList()
expect(wrapper.vm.isOpen).toBe(false)
wrapper.unmount()
})
it('emit the value when returnObject is false', async () => {
const wrapper = mount(SySelect, {
props: {
returnObject: false,
items: [{ text: 'Option 1', value: '1' }, { text: 'Option 2', value: '2' }],
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
const firstItem = wrapper.findComponent(VList).findAll('.v-list-item').at(0)
await firstItem!.trigger('click')
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual(['1'])
await wrapper.find('.v-field').trigger('click')
const secondItem = wrapper.findComponent(VList).findAll('.v-list-item').at(1)
await secondItem!.trigger('click')
expect(wrapper.emitted()['update:modelValue']?.[1]).toEqual(['2'])
wrapper.unmount()
})
it('emit the object when returnObject is true', async () => {
const wrapper = mount(SySelect, {
props: {
returnObject: true,
items: [{ text: 'Option 1', value: '1' }, { text: 'Option 2', value: '2' }],
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
const firstItem = wrapper.findComponent(VList).findAll('.v-list-item').at(0)
await firstItem!.trigger('click')
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual([{ text: 'Option 1', value: '1' }])
await wrapper.find('.v-field').trigger('click')
const secondItem = wrapper.findComponent(VList).findAll('.v-list-item').at(1)
await secondItem!.trigger('click')
expect(wrapper.emitted()['update:modelValue']?.[1]).toEqual([{ text: 'Option 2', value: '2' }])
wrapper.unmount()
})
it('emit the value when returnObject is false with textKey and keyValue set', async () => {
const wrapper = mount(SySelect, {
props: {
returnObject: false,
textKey: 'theText',
valueKey: 'theValue',
items: [{ theText: 'Option 1', theValue: '1' }, { theText: 'Option 2', theValue: '2' }],
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
const firstItem = wrapper.findComponent(VList).findAll('.v-list-item').at(0)
await firstItem!.trigger('click')
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual(['1'])
await wrapper.find('.v-field').trigger('click')
const secondItem = wrapper.findComponent(VList).findAll('.v-list-item').at(1)
await secondItem!.trigger('click')
expect(wrapper.emitted()['update:modelValue']?.[1]).toEqual(['2'])
wrapper.unmount()
})
it('emit the object when returnObject is true with textKey and keyValue set', async () => {
const wrapper = mount(SySelect, {
props: {
returnObject: true,
textKey: 'theText',
valueKey: 'theValue',
items: [{ theText: 'Option 1', theValue: '1' }, { theText: 'Option 2', theValue: '2' }],
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
const firstItem = wrapper.findComponent(VList).findAll('.v-list-item').at(0)
await firstItem!.trigger('click')
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual([{ theText: 'Option 1', theValue: '1' }])
await wrapper.find('.v-field').trigger('click')
const secondItem = wrapper.findComponent(VList).findAll('.v-list-item').at(1)
await secondItem!.trigger('click')
expect(wrapper.emitted()['update:modelValue']?.[1]).toEqual([{ theText: 'Option 2', theValue: '2' }])
wrapper.unmount()
})
it('emit the value when items is an array of string', async () => {
const wrapper = mount(SySelect, {
props: {
items: ['Option 1', 'Option 2'] as unknown as ItemType[],
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
const firstItem = wrapper.findComponent(VList).findAll('.v-list-item').at(0)
await firstItem!.trigger('click')
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual(['Option 1'])
await wrapper.find('.v-field').trigger('click')
const secondItem = wrapper.findComponent(VList).findAll('.v-list-item').at(1)
await secondItem!.trigger('click')
expect(wrapper.emitted()['update:modelValue']?.[1]).toEqual(['Option 2'])
wrapper.unmount()
})
it('is clearable when clearable is true', async () => {
const wrapper = mount(SySelect, {
props: {
modelValue: '1',
clearable: true,
items: [{ text: 'Option 1', value: '1' }, { text: 'Option 2', value: '2' }],
},
attachTo: document.body,
})
const clearBtn = wrapper.find('.sy-select__clear-icon')
expect(clearBtn.exists()).toBe(true)
await clearBtn.trigger('click')
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual([null])
wrapper.unmount()
})
describe('Multiple selection mode', () => {
it('handles multiple selection correctly', async () => {
const items = [
{ text: '-choisir-', value: null },
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
{ text: 'Option 3', value: '3' },
]
const wrapper = mount(SySelect, {
props: {
items,
multiple: true,
modelValue: [],
textKey: 'text',
valueKey: 'value',
},
attachTo: document.body,
})
// Open the select menu
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
// Select Option 1
const listItems = wrapper.findComponent(VList).findAll('.v-list-item')
await listItems[1]?.trigger('click')
await wrapper.vm.$nextTick()
// Check that Option 1 is selected
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual([['1']])
// Select Option 2 as well
await listItems[2]?.trigger('click')
await wrapper.vm.$nextTick()
// Check that both options are selected
expect(wrapper.emitted()['update:modelValue']?.[1]).toEqual([['1', '2']])
wrapper.unmount()
})
it('clears all selections when default option is clicked', async () => {
const items = [
{ text: '-choisir-', value: null },
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
]
const wrapper = mount(SySelect, {
props: {
items,
multiple: true,
modelValue: ['1', '2'],
textKey: 'text',
valueKey: 'value',
},
attachTo: document.body,
})
// Open the select menu
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
// Click on the default option
const defaultOption = wrapper.findComponent(VList).findAll('.v-list-item')[0]!
await defaultOption.trigger('click')
await wrapper.vm.$nextTick()
// Check that all selections are cleared
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual([[]])
wrapper.unmount()
})
it('treats default option as selected when no items are selected', async () => {
const items = [
{ text: '-choisir-', value: null },
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
]
const wrapper = mount(SySelect, {
props: {
items,
multiple: true,
modelValue: [],
textKey: 'text',
valueKey: 'value',
},
attachTo: document.body,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
// Check that the selectedItemText is the default option
expect(instance.selectedItemText).toBe('-choisir-')
// Check that isDefaultOption returns true for the default item
const defaultItem = items[0]
expect(instance.isDefaultOption(defaultItem)).toBe(true)
// Check that isItemSelected returns true for the default item when no selections
expect(instance.isItemSelected(defaultItem)).toBe(true)
wrapper.unmount()
})
})
describe('Chips mode', () => {
it('renders chips for selected items', async () => {
const items = [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
{ text: 'Option 3', value: '3' },
]
const wrapper = mount(SySelect, {
props: {
items,
multiple: true,
chips: true,
modelValue: ['1', '2'],
textKey: 'text',
valueKey: 'value',
},
attachTo: document.body,
})
// Check that chips are rendered
const chips = wrapper.findAll('.v-chip')
expect(chips.length).toBe(2)
expect(chips[0]?.text()).toBe('Option 1')
expect(chips[1]?.text()).toBe('Option 2')
wrapper.unmount()
})
it('removes a chip when close button is clicked', async () => {
const items = [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
{ text: 'Option 3', value: '3' },
]
const wrapper = mount(SySelect, {
props: {
items,
multiple: true,
chips: true,
modelValue: ['1', '2'],
textKey: 'text',
valueKey: 'value',
},
attachTo: document.body,
})
// Find the first chip's close button and click it
const closeButton = wrapper.find('.v-chip__close')!
await closeButton.trigger('click')
await wrapper.vm.$nextTick()
// Check that the chip was removed from the model
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual([['2']])
wrapper.unmount()
})
it('handles chip text correctly for object items', async () => {
const items = [
{ text: 'Option 1', value: '1', data: { id: 101 } },
{ text: 'Option 2', value: '2', data: { id: 102 } },
]
const wrapper = mount(SySelect, {
props: {
items,
multiple: true,
chips: true,
returnObject: true,
modelValue: [items[0]!, items[1]!],
textKey: 'text',
valueKey: 'value',
},
attachTo: document.body,
})
// Check that chips display the correct text
const chips = wrapper.findAll('.v-chip')
expect(chips.length).toBe(2)
expect(chips[0]?.text()).toBe('Option 1')
expect(chips[1]?.text()).toBe('Option 2')
wrapper.unmount()
})
it('safely handles different item types in chips', async () => {
// This test verifies our safeChipItem function works correctly
const items = [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: 2 }, // Number value
]
const wrapper = mount(SySelect, {
props: {
items,
multiple: true,
chips: true,
modelValue: ['1', 2],
textKey: 'text',
valueKey: 'value',
},
attachTo: document.body,
})
// Check that chips are rendered without errors
const chips = wrapper.findAll('.v-chip')
expect(chips.length).toBe(2)
expect(chips[0]?.text()).toBe('Option 1')
expect(chips[1]?.text()).toBe('Option 2')
// Test the safeChipItem method directly
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
const instance = wrapper.vm as any
const stringResult = instance.safeChipItem('test')
const numberResult = instance.safeChipItem(123)
const objectResult = instance.safeChipItem({ id: 3 })
expect(stringResult).toBe('test')
expect(numberResult).toBe(123)
expect(typeof objectResult).toBe('object')
wrapper.unmount()
})
it('removes chip when Enter key is pressed on chip close button', async () => {
const items = [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
]
const wrapper = mount(SySelect, {
props: {
items,
multiple: true,
chips: true,
modelValue: ['1', '2'],
textKey: 'text',
valueKey: 'value',
},
attachTo: document.body,
})
await wrapper.vm.$nextTick()
const closeButton = wrapper.find('.v-chip__close')
closeButton.element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }))
await wrapper.vm.$nextTick()
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual([['2']])
wrapper.unmount()
})
it('removes chip when Space key is pressed on chip close button', async () => {
const items = [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
]
const wrapper = mount(SySelect, {
props: {
items,
multiple: true,
chips: true,
modelValue: ['1', '2'],
textKey: 'text',
valueKey: 'value',
},
attachTo: document.body,
})
await wrapper.vm.$nextTick()
const closeButton = wrapper.find('.v-chip__close')
closeButton.element.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true }))
await wrapper.vm.$nextTick()
expect(wrapper.emitted()['update:modelValue']?.[0]).toEqual([['2']])
wrapper.unmount()
})
})
describe('keyboard navigation', () => {
it('navigates options with arrow keys', async () => {
const items = [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
{ text: 'Option 3', value: '3' },
]
const wrapper = mount(SySelect, {
props: {
items,
modelValue: null,
textKey: 'text',
valueKey: 'value',
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
const list = wrapper.findComponent(VList).find('.v-list')!
// expect the first item to be highlighted
const firstItem = wrapper.findComponent(VList).findAll('.v-list-item').at(0)
await wrapper.vm.$nextTick()
expect(firstItem?.classes()).toContain('active')
expect(firstItem?.attributes('tabindex')).toBe('0')
await list.trigger('keydown.down')
await wrapper.vm.$nextTick()
const secondItem = wrapper.findComponent(VList).findAll('.v-list-item').at(1)
expect(firstItem?.classes()).not.toContain('keyboard-focused')
expect(firstItem?.attributes('tabindex')).toBe('-1')
expect(secondItem?.classes()).toContain('keyboard-focused')
expect(secondItem?.attributes('tabindex')).toBe('0')
wrapper.unmount()
})
it('selects an option with enter key', async () => {
const items = [
{ text: 'Option 1', value: '1' },
{ text: 'Option 2', value: '2' },
{ text: 'Option 3', value: '3' },
]
const wrapper = mount(SySelect, {
props: {
items,
modelValue: null,
textKey: 'text',
valueKey: 'value',
},
attachTo: document.body,
})
await wrapper.find('.v-field').trigger('click')
await wrapper.vm.$nextTick()
const list = wrapper.findComponent(VList).find('.v-list')!
await list.trigger('keydown.down')
await wrapper.vm.$nextTick()
await list.trigger('keydown.enter')
await wrapper.vm.$nextTick()
expect(wrapper.emitted()['update:modelValue']).toEqual([['2']])
wrapper.unmount()
})
})
})