import { nextTick } from 'vue' import { NOOP } from '@vue/shared' import { EVENT_CODE } from '@element-plus/utils/aria' import { makeMountFunc } from '@element-plus/test-utils/make-mount' import Select from '../src/select.vue' jest.useFakeTimers() const _mount = makeMountFunc({ components: { 'el-select': Select, }, }) const createData = (count = 1000) => { const initials = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] return Array.from({ length: count }).map((_, idx) => ({ value: `option_${idx + 1}`, label: `${initials[idx % 10]}${idx}`, })) } const clickClearButton = async wrapper => { const select = wrapper.findComponent(Select) const selectVm = select.vm as any selectVm.states.comboBoxHovering = true await nextTick const clearBtn = wrapper.find(`.${selectVm.clearIcon}`) expect(clearBtn.exists()).toBeTruthy() await clearBtn.trigger('click') } interface SelectProps { popperClass?: string value?: string | string[] | number | number[] options?: any[] disabled?: boolean clearable?: boolean multiple?: boolean filterable?: boolean multipleLimit?: number allowCreate?: boolean popperAppendToBody?: boolean placeholder?: string [key: string]: any } interface SelectEvents { onChange?: (value?: string) => void onVisibleChange?: (visible?: boolean) => void onRemoveTag?: (tag?: string) => void onFocus?: (event?: FocusEvent) => void onBlur?: (event?) => void [key: string]: (...args) => any } const createSelect = (options: { data?: () => SelectProps methods?: SelectEvents slots?: { empty?: string default?: string } } = {}) => { const emptySlot = (options.slots && options.slots.empty && ``) || '' const defaultSlot = (options.slots && options.slots.default && ``) || '' return _mount(` ${defaultSlot} ${emptySlot} `, { data () { return { options: createData(), value: '', popperClass: '', allowCreate: false, disabled: false, clearable: false, multiple: false, filterable: false, multipleLimit: 0, popperAppendToBody: true, placeholder: DEFAULT_PLACEHOLDER, ...options.data && options.data(), } }, methods: { onChange: NOOP, onVisibleChange: NOOP, onRemoveTag: NOOP, onFocus: NOOP, onBlur: NOOP, ...options.methods, }, }) } function getOptions(): HTMLElement[] { return Array.from(document.querySelectorAll( `body > div:last-child .${OPTION_ITEM_CLASS_NAME}`, )) } const CLASS_NAME = 'el-select-v2' const WRAPPER_CLASS_NAME = 'el-select-v2__wrapper' const OPTION_ITEM_CLASS_NAME = 'el-select-dropdown__option-item' const PLACEHOLDER_CLASS_NAME = 'el-select-v2__placeholder' const DEFAULT_PLACEHOLDER = 'Select' describe('Select', () => { afterEach(() => { document.body.innerHTML = '' }) it('create', async () => { const wrapper = createSelect() await nextTick expect(wrapper.classes()).toContain(CLASS_NAME) expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toContain(DEFAULT_PLACEHOLDER) const select = wrapper.findComponent(Select) await wrapper.trigger('click') expect((select.vm as any).expanded).toBeTruthy() }) it('options rendered correctly', async() => { const wrapper = createSelect() await nextTick const vm = wrapper.vm as any const options = document.getElementsByClassName(OPTION_ITEM_CLASS_NAME) const result = [].every.call(options, (option, index) => { const text = option.textContent return text === vm.options[index].label }) expect(result).toBeTruthy() }) it('custom dropdown class', async() => { createSelect({ data: () => ({ popperClass: 'custom-dropdown', }), }) await nextTick expect(document.querySelector('.el-popper').classList).toContain('custom-dropdown') }) it('default value', async () => { const wrapper = createSelect({ data: () => ({ value: '2', options: [ { value: '1', label: 'option_a', }, { value: '2', label: 'option_b', }, { value: '3', label: 'option_c', }, ], }), }) const vm = wrapper.vm as any await nextTick expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe(vm.options[1].label) }) it('default value is null or undefined', async () => { const wrapper = createSelect() const vm = wrapper.vm as any const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) vm.value = vm.options[2].value await nextTick expect(placeholder.text()).toBe(vm.options[2].label) vm.value = null await nextTick expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) }) it('sync set value and options', async () => { const wrapper = createSelect() await nextTick const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) const vm = wrapper.vm as any vm.value = vm.options[1].value await nextTick expect(placeholder.text()).toBe(vm.options[1].label) vm.options[1].label = 'option bb aa' await nextTick expect(placeholder.text()).toBe('option bb aa') }) it('single select', async () => { const wrapper = createSelect({ data() { return { count: 0, } }, methods: { onChange() { this.count++ }, }, }) await nextTick const options = getOptions() const vm = wrapper.vm as any const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) expect(vm.value).toBe('') expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) options[2].click() await nextTick expect(vm.value).toBe(vm.options[2].value) expect(placeholder.text()).toBe(vm.options[2].label) options[4].click() await nextTick expect(vm.value).toBe(vm.options[4].value) expect(placeholder.text()).toBe(vm.options[4].label) expect(vm.count).toBe(2) }) it('disabled option', async () => { const wrapper = createSelect({ data: () => { return { options: [ { value: '1', label: 'option 1', disabled: false, }, { value: '2', label: 'option 2', disabled: true, }, { value: '3', label: 'option 3', disabled: false, }, ], } }, }) await nextTick const vm = wrapper.vm as any const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) const option = document.querySelector(`.el-select-dropdown__option-item.is-disabled`) expect(option.textContent).toBe(vm.options[1].label) option.click() await nextTick expect(vm.value).toBe('') expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) vm.options[2].disabled = true await nextTick const options = document.querySelectorAll(`.el-select-dropdown__option-item.is-disabled`) expect(options.length).toBe(2) expect(options.item(1).textContent).toBe(vm.options[2].label) options.item(1).click() await nextTick expect(vm.value).toBe('') expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) }) it('disabled select', async () => { const wrapper = createSelect({ data: () => { return { disabled: true, } }, }) await nextTick expect(wrapper.find(`.${WRAPPER_CLASS_NAME}`).classes()).toContain('is-disabled') }) it('visible event', async () => { const wrapper = createSelect({ data: () => { return { visible: false, } }, methods: { onVisibleChange(visible) { this.visible = visible }, }, }) await nextTick const vm = wrapper.vm as any await wrapper.trigger('click') expect(vm.visible).toBeTruthy() }) it('clearable', async () => { const wrapper = createSelect({ data: () => ({ clearable: true }), }) const vm = wrapper.vm as any vm.value = vm.options[1].value await nextTick await clickClearButton(wrapper) expect(vm.value).toBe('') const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) }) describe('multiple', () => { it('multiple select', async () => { const wrapper = createSelect({ data: () => { return { multiple: true, value: [], } }, }) await nextTick const vm = wrapper.vm as any const options = getOptions() options[1].click() await nextTick expect(vm.value.length).toBe(1) expect(vm.value[0]).toBe(vm.options[1].value) options[3].click() await nextTick expect(vm.value.length).toBe(2) expect(vm.value[1]).toBe(vm.options[3].value) const tagIcon = wrapper.find('.el-tag__close') await tagIcon.trigger('click') expect(vm.value.length).toBe(1) }) it('remove-tag', async () => { const wrapper = createSelect({ data() { return { removeTag: '', multiple: true, } }, methods: { onRemoveTag(tag) { this.removeTag = tag }, }, }) await nextTick const vm = wrapper.vm as any const options = getOptions() options[0].click() await nextTick() options[1].click() await nextTick() options[2].click() await nextTick() expect(vm.value.length).toBe(3) const tagCloseIcons = wrapper.findAll('.el-tag__close') await tagCloseIcons[1].trigger('click') expect(vm.value.length).toBe(2) await tagCloseIcons[0].trigger('click') expect(vm.value.length).toBe(1) }) it('limit', async () => { const wrapper = createSelect({ data() { return { multiple: true, multipleLimit: 2, value: [], } }, }) await nextTick const vm = wrapper.vm as any const options = getOptions() options[1].click() await nextTick options[2].click() await nextTick expect(vm.value.length).toBe(2) options[3].click() await nextTick expect(vm.value.length).toBe(2) }) }) describe('event', () => { it('focus & blur', async () => { const onFocus = jest.fn() const onBlur = jest.fn() const wrapper = createSelect({ methods: { onFocus, onBlur, }, }) const input = wrapper.find('input') const select = wrapper.findComponent(Select) await input.trigger('focus') const selectVm = select.vm as any // Simulate focus state to trigger menu multiple times selectVm.toggleMenu() await nextTick selectVm.toggleMenu() await nextTick // Simulate click the outside selectVm.handleClickOutside() await nextTick expect(onFocus).toHaveBeenCalledTimes(1) expect(onBlur).toHaveBeenCalled() }) it('focus & blur for multiple & filterable select', async () => { const onFocus = jest.fn() const onBlur = jest.fn() const wrapper = createSelect({ data() { return { multiple: true, filterable: true, value: [], } }, methods: { onFocus, onBlur, }, }) const input = wrapper.find('input') const select = wrapper.findComponent(Select) await input.trigger('focus') const selectVm = select.vm as any // Simulate focus state to trigger menu multiple times selectVm.toggleMenu() await nextTick selectVm.toggleMenu() await nextTick // Select multiple items in multiple mode without triggering focus const options = getOptions() options[1].click() await nextTick options[2].click() await nextTick expect(onFocus).toHaveBeenCalledTimes(1) // Simulate click the outside selectVm.handleClickOutside() await nextTick await nextTick expect(onBlur).toHaveBeenCalled() }) it('only emit change on user input', async () => { const handleChanged = jest.fn() const wrapper = createSelect({ methods: { onChange: handleChanged, }, }) await nextTick const vm = wrapper.vm as any vm.value = 'option_2' await nextTick expect(handleChanged).toHaveBeenCalledTimes(0) const options = getOptions() options[4].click() await nextTick expect(handleChanged).toHaveBeenCalled() }) }) describe('allow-create', () => { it('single select', async() => { const wrapper = createSelect({ data: () => { return { allowCreate: true, filterable: true, clearable: true, options: [ { value: '1', label: 'option 1', }, { value: '2', label: 'option 2', }, { value: '3', label: 'option 3', }, ], } }, }) await nextTick const vm = wrapper.vm as any const input = wrapper.find('input') await wrapper.trigger('click') // create a new option await input.trigger('compositionupdate', { data: '1111', }) const options = getOptions() const select = wrapper.findComponent(Select) const selectVm = select.vm as any expect(selectVm.filteredOptions.length).toBe(1) // selected the new option await options[0].click() expect(vm.value).toBe('1111') // closed the menu await wrapper.trigger('click') expect(selectVm.filteredOptions.length).toBe(4) selectVm.handleClear() expect(selectVm.filteredOptions.length).toBe(3) }) it('multiple', async () => { const wrapper = createSelect({ data: () => { return { allowCreate: true, filterable: true, clearable: true, multiple: true, options: [ { value: '1', label: 'option 1', }, { value: '2', label: 'option 2', }, { value: '3', label: 'option 3', }, ], } }, }) await nextTick const vm = wrapper.vm as any await wrapper.trigger('click') await wrapper.find('input').trigger('compositionupdate', { data: '1111', }) const options = getOptions() const select = wrapper.findComponent(Select) const selectVm = select.vm as any expect(selectVm.filteredOptions.length).toBe(1) // selected the new option await options[0].click() // closed the menu await wrapper.trigger('click') await wrapper.find('input').trigger('compositionupdate', { data: '2222', }) await getOptions()[0].click() expect(JSON.stringify(vm.value)).toBe(JSON.stringify(['1111', '2222'])) await wrapper.trigger('click') expect(selectVm.filteredOptions.length).toBe(5) // remove tag const tagCloseIcons = wrapper.findAll('.el-tag__close') await tagCloseIcons[1].trigger('click') expect(selectVm.filteredOptions.length).toBe(4) // simulate backspace await wrapper.find('input').trigger('keydown', { key: EVENT_CODE.backspace, }) expect(selectVm.filteredOptions.length).toBe(3) }) }) it('render empty slot', async () => { const wrapper = createSelect({ data () { return { options: [], popperAppendToBody: false, } }, slots: { empty: '
EmptySlot
', }, }) await nextTick expect(wrapper.find('.empty-slot').exists()).toBeTruthy() }) it('should set placeholder to label of selected option when filterable is true and multiple is false', async () => { const wrapper = createSelect({ data () { return { options: [ { value: '1', label: 'option 1', }, { value: '2', label: 'option 2', }, { value: '3', label: 'option 3', }, ], filterable: true, multiple: false, } }, }) await nextTick const vm = wrapper.vm as any const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) vm.value = '2' await nextTick const select = wrapper.findComponent(Select) const selectVm = select.vm as any selectVm.toggleMenu() const input = wrapper.find('input') await input.trigger('focus') expect(placeholder.text()).toBe('option 2') }) it('default value is null or undefined', async () => { const wrapper = createSelect({ data() { return { value: null, } }, }) await nextTick const vm = wrapper.vm as any const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) vm.value = undefined await nextTick expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) }) it('emptyText error show', async () => { const wrapper = createSelect({ data () { return { value: `${Math.random()}`, } }, }) await nextTick const vm = wrapper.vm as any const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) expect(placeholder.text()).toBe(vm.value) }) it('customized option renderer', async () => { const wrapper = createSelect({ data () { return { popperAppendToBody: false, } }, slots: { default: `
{{ item.label }} {{ item.value }}
`, }, }) await nextTick expect(wrapper.findAll('.custom-renderer').length).toBeGreaterThan(0) }) it('tag of disabled option is not closable', async () => { const wrapper = createSelect({ data () { return { multiple: true, options: [ { value: 1, lable: 'option 1', disabled: true, }, { value: 2, lable: 'option 2', disabled: true, }, { value: 3, lable: 'option 3', }, ], value: [2, 3], } }, }) await nextTick expect(wrapper.findAll('.el-tag').length).toBe(2) const tagCloseIcons = wrapper.findAll('.el-tag__close') expect(tagCloseIcons.length).toBe(1) await tagCloseIcons[0].trigger('click') expect(wrapper.findAll('.el-tag__close').length).toBe(0) expect(wrapper.findAll('.el-tag').length).toBe(1) }) it('modelValue should be deep reactive in multiple mode', async () => { const wrapper = createSelect({ data () { return { multiple: true, value: ['option_1', 'option_2', 'option_3'], } }, }) await nextTick expect(wrapper.findAll('.el-tag').length).toBe(3) const vm = wrapper.vm as any vm.value.splice(0, 1) await nextTick expect(wrapper.findAll('.el-tag').length).toBe(2) }) it('should reset placeholder after clear when both multiple and filterable are true', async () => { const wrapper = createSelect({ data () { return { value: ['option_1'], clearable: true, filterable: true, multiple: true, } }, }) await nextTick expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).exists()).toBeFalsy() // When all tags are removed, the placeholder should be displayed const tagCloseIcon = wrapper.find('.el-tag__close') await tagCloseIcon.trigger('click') expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe(DEFAULT_PLACEHOLDER) // The placeholder should disappear after it is selected again const options = getOptions() options[0].click() await nextTick expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).exists()).toBeFalsy() // Simulate keyboard events const selectInput = wrapper.find('input') selectInput.trigger('keydown', { key: EVENT_CODE.backspace, }) await nextTick expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe(DEFAULT_PLACEHOLDER) }) })