element-plus/packages/components/select-v2/__tests__/select.test.ts

1550 lines
41 KiB
TypeScript
Raw Normal View History

import { nextTick } from 'vue'
import { NOOP } from '@vue/shared'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { hasClass } from '@element-plus/utils'
import { EVENT_CODE } from '@element-plus/constants'
import { makeMountFunc } from '@element-plus/test-utils/make-mount'
import { rAF } from '@element-plus/test-utils/tick'
import { CircleClose } from '@element-plus/icons-vue'
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
import Select from '../src/select.vue'
vi.mock('lodash-unified', async () => {
return {
...((await vi.importActual('lodash-unified')) as Record<string, any>),
debounce: vi.fn((fn) => {
fn.cancel = vi.fn()
fn.flush = vi.fn()
return fn
}),
}
})
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()
epic: refactor icons (#3492) * refactor(components): el-alert icon (#3441) * refactor(components): el-alert icon * fix: el-alert test * fix: [el-alert] lint error * feat(utils): add icon util * refactor(components): el-alert icon * refactor(utils): icon * test: update alert test * refactor: remove el-icon in util * refactor(components): el-notification icon (#3512) * chore: temp save changes * refactor: update icon * fix: var * docs: update icon * fix: type class error * style: remove unused class * fix: update icon type * docs: update icon * fix: update iconComponent * refactor(components): el-dialog icon (#3505) * refactor(components): [el-dialog] icon * fix: typo * refactor: dialog icon ref * refactor: dialog icon * style: update close button size * refactor(components): el-message-box icon (#3507) * refactor(components): message-box icon * refactor(components): message icon * fix: test * refactor: update ref * fix: test * docs: update icon * fix: delete unused code * docs: update type * fix: update close button size * fix: type * fix: icon type class * fix: icon type * fix: update type class * refactor(components): el-popconfirm icon (#3513) * refactor(components): el-popconfirm icon * fix: space styles * docs: update example * docs: update example for trigger event * refactor(components): el-page-header icon (#3518) * refactor(components): el-page-header icon * docs: update icon * fix: space errors * refactor(components): el-steps icon (#3520) * refactor(components): el-backtop icon (#3514) * refactor(components): el-backtop icon * fix: update font size * refactor(components): el-button icon (#3536) * refactor(components): el-button icon * fix: update example * refactor(components): el-button icon * refactor: style simplify Co-authored-by: imswk <syfzxx@vip.qq.com> * refactor(components): el-tree icon (#3590) Co-authored-by: qianjie <qianjie@patsnap.com> * refactor(components): el-avatar icon (#3599) Co-authored-by: qianjie <qianjie@patsnap.com> * refactor(components): el-carousel icon (#3602) Co-authored-by: qianjie <qianjie@patsnap.com> * refactor(components): el-progress icon (#3607) Co-authored-by: qianjie <qianjie@patsnap.com> * refactor(components): el-result icon (#3609) Co-authored-by: qianjie <qianjie@patsnap.com> * refactor(components): el-timeline icon (#3646) Co-authored-by: qianjie <qianjie@patsnap.com> * refactor(components): el-table icon (#3611) Co-authored-by: qianjie <qianjie@patsnap.com> * chore: add vue to gitignore * fix: buildProp error * refactor(components): el-pagination icon (#3605) * test: el-menu deprecated icon font (#3794) * test: el-loading deprecated icon font (#3795) * chore: Update changelog 1.1.0-beta.20 (#3838) * fix: about custom extension of table column (#3437) * refactor(components): el-autocomplete icon (#3783) * refactor(components): el-dropdown icon (#3517) * refactor(components): el-dropdown icon * fix: arrow down icon size * style: revert * fix: font-size * refactor(components): el-link icon (#3538) * refactor(components): el-link icon * refactor: update example * fix: import icons * docs: add tip * refactor: update link icon * fix: icon align * fix: typo * refactor(components): el-select icon (#3780) * refactor(components): el-input icon (#3565) * refactor(components): input icon * docs: update example * refactor(components): el-select icon * docs: icon usage * fix: input style * fix: add scoped * docs: fix icon * fix: icon align * fix: icon align * fix: typo * fix: typo * refactor(components): el-input-number icon (#3566) * refactor(components): el-input-number icon * fix: icon align * refactor(components): el-tag icon (#3793) * refactor(components): el-tag icon * fix: icon align * fix: icon margin * refactor(components): el-switch icon (#3787) * refactor(components): el-switch icon * fix: icon align * docs: add icon usage * refactor(components): el-collapse icon (#3788) * refactor(components): el-collapse icon * docs: update icon demo * fix: typo * refactor(components): el-uplod icon (#3598) * refactor(components): el-uplod icon * docs: update icon useage * docs: fix icon * docs: fix icon * docs: update upload-filled icon * fix: icon align * fix: item icon align * refactor(components): el-select-v2 icon (#3781) * refactor(components): el-select-v2 icon * revert: play/main.ts * fix: icon style * fix: icon solt * refactor(components): el-cascader icon (#3785) * refactor(components): input icon * docs: update example * refactor(components): el-select icon * refactor(components): el-cascader icon * refactor(components): el-tag icon * fix: icon align * fix: icon margin * docs: icon usage * fix: input style * fix: add scoped * test: remove unuesd code * fix: cascader-panel * fix: icon style * fix: icon animation * refactor(components): el-drawer icon (#3796) * refactor(components): el-drawer icon * fix: close icon size * fix: close icon align * refactor(components): el-transfer icon (#3792) * refactor(components): el-transfer icon * fix: icon align * refactor(components): el-image-viewer icon (#3797) * refactor(components): el-image-view icon * fix: icon size * docs: update domo * style: format vue * fix: fullscreen icon * Add cursor for icons Co-authored-by: jeremywuuuuu <15975785+JeremyWuuuuu@users.noreply.github.com> * refactor(components): rate-icon (#3891) * refactor(components): el-rate icon * docs: update example * docs: update * test: update * docs: update * fix: variable rename * fix: demo update * fix: allow half bug * refactor(components): el-time-picker icon (#3834) * refactor(components): el-button icon * refactor(components): date-picker icon * refactor(components): input icon * docs: update example * refactor(components): el-select icon * refactor(components): el-time-select icon * fix: icon width * docs: icon usage * fix: input style * fix: add scoped * fix: icon size * docs: icon display * fix: icon in button * docs: fix error * refactor(components): el-time-picker icon * fix: var * fix: icon align * test: delete unused code * test: fix error * fix: remove unused code * docs: fix typo * docs: update icon * fix: default prefix icon * fix: range picker icon align * Update cursor for picker Co-authored-by: jeremywuuuuu <15975785+JeremyWuuuuu@users.noreply.github.com> * fix(components): el-input close icon * chore: delete fonts * fix: copy full style Co-authored-by: imswk <syfzxx@vip.qq.com> Co-authored-by: bastarder <85257684@qq.com> Co-authored-by: qianjie <qianjie@patsnap.com> Co-authored-by: Element <wallement@gmail.com> Co-authored-by: Alan Wang <948467222@qq.com> Co-authored-by: jeremywuuuuu <15975785+JeremyWuuuuu@users.noreply.github.com>
2021-10-27 23:17:13 +08:00
const clearBtn = wrapper.findComponent(CircleClose)
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
collapseTags?: boolean
collapseTagsTooltip?: boolean
filterable?: boolean
remote?: 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
filterMethod?: (query: string) => void | undefined
remoteMethod?: (query: string) => void | undefined
[key: string]: (...args) => any
}
const createSelect = (
options: {
data?: () => SelectProps
methods?: SelectEvents
slots?: {
empty?: string
default?: string
}
} = {}
) => {
const emptySlot =
(options.slots &&
options.slots.empty &&
`<template #empty>${options.slots.empty}</template>`) ||
''
const defaultSlot =
(options.slots &&
options.slots.default &&
`<template #default="{item}">${options.slots.default}</template>`) ||
''
return _mount(
`
<el-select
:options="options"
:popper-class="popperClass"
:value-key="valueKey"
:disabled="disabled"
:clearable="clearable"
:multiple="multiple"
:collapseTags="collapseTags"
:collapseTagsTooltip="collapseTagsTooltip"
:filterable="filterable"
:multiple-limit="multipleLimit"
:placeholder="placeholder"
:allow-create="allowCreate"
:remote="remote"
:reserve-keyword="reserveKeyword"
:scrollbar-always-on="scrollbarAlwaysOn"
:teleported="teleported"
${
options.methods && options.methods.filterMethod
? `:filter-method="filterMethod"`
: ''
}
${
options.methods && options.methods.remoteMethod
? `:remote-method="remoteMethod"`
: ''
}
@change="onChange"
@visible-change="onVisibleChange"
@remove-tah="onRemoveTag"
@focus="onFocus"
@blur="onBlur"
v-model="value">
${defaultSlot}
${emptySlot}
</el-select>
`,
{
data() {
return {
options: createData(),
value: '',
popperClass: '',
allowCreate: false,
valueKey: 'value',
disabled: false,
clearable: false,
multiple: false,
collapseTags: false,
collapseTagsTooltip: false,
remote: false,
filterable: false,
reserveKeyword: false,
multipleLimit: 0,
placeholder: DEFAULT_PLACEHOLDER,
scrollbarAlwaysOn: false,
popperAppendToBody: undefined,
teleported: undefined,
...(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<HTMLElement>(`.${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 = Array.from(
document.querySelectorAll(`.${OPTION_ITEM_CLASS_NAME}`)
)
const result = options.every((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('default value is Object', async () => {
const wrapper = createSelect({
data: () => ({
valueKey: 'value',
value: {
value: '1',
label: 'option_a',
},
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[0].label
)
expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe(
vm.value.label
)
})
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('value-key option', async () => {
const wrapper = createSelect({
data: () => {
return {
options: [
{
id: 'id 1',
value: 'value 1',
label: 'option 1',
},
{
id: 'id 2',
value: 'value 2',
label: 'option 2',
},
{
id: 'id 3',
value: 'value 3',
label: 'option 3',
},
],
value: '',
valueKey: 'id',
}
},
})
await nextTick()
const vm = wrapper.vm as any
const options = getOptions()
options[1].click()
await nextTick()
expect(vm.value).toBe(vm.options[1].id)
vm.valueKey = 'value'
await nextTick()
options[2].click()
await nextTick()
expect(vm.value).toBe(vm.options[2].value)
})
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<HTMLElement>(
`.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<HTMLElement>(
`.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')
await nextTick()
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)
})
it('value-key option', async () => {
const wrapper = createSelect({
data: () => {
return {
options: [
{
id: 'id 1',
value: 'value 1',
label: 'option 1',
},
{
id: 'id 2',
value: 'value 2',
label: 'option 2',
},
{
id: 'id 3',
value: 'value 3',
label: 'option 3',
},
],
multiple: true,
value: [],
valueKey: 'id',
}
},
})
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].id)
vm.valueKey = 'value'
await nextTick()
options[2].click()
await nextTick()
expect(vm.value.length).toBe(2)
expect(vm.value[1]).toBe(vm.options[2].value)
})
})
describe('collapseTags', () => {
it('use collapseTags', async () => {
const wrapper = createSelect({
data: () => {
return {
multiple: true,
collapseTags: true,
value: [],
}
},
})
await nextTick()
const vm = wrapper.vm as any
const options = getOptions()
options[0].click()
await nextTick()
expect(vm.value.length).toBe(1)
expect(vm.value[0]).toBe(vm.options[0].value)
options[1].click()
await nextTick()
options[2].click()
await nextTick()
expect(vm.value.length).toBe(3)
const tags = wrapper.findAll('.el-tag').filter((item) => {
return !hasClass(item.element, 'in-tooltip')
})
expect(tags.length).toBe(2)
})
it('use collapseTagsTooltip', async () => {
const wrapper = createSelect({
data: () => {
return {
multiple: true,
collapseTags: true,
collapseTagsTooltip: true,
value: [],
}
},
})
await nextTick()
const vm = wrapper.vm as any
const options = getOptions()
options[0].click()
await nextTick()
expect(vm.value.length).toBe(1)
expect(vm.value[0]).toBe(vm.options[0].value)
options[1].click()
await nextTick()
options[2].click()
await nextTick()
expect(vm.value.length).toBe(3)
expect(wrapper.findAll('.el-tag')[4].element.textContent).toBe('c2')
})
})
describe('manually set modelValue', () => {
it('set modelValue in single select', async () => {
const wrapper = createSelect({
data: () => {
return {
value: '',
}
},
})
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[0].click()
await nextTick()
expect(vm.value).toBe(vm.options[0].value)
expect(placeholder.text()).toBe(vm.options[0].label)
const option = vm.options[0].value
vm.value = ''
await nextTick()
expect(vm.value).toBe('')
expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER)
vm.value = option
await nextTick()
expect(vm.value).toBe('option_1')
expect(placeholder.text()).toBe('a0')
})
it('set modelValue in multiple select', async () => {
const wrapper = createSelect({
data: () => {
return {
multiple: true,
value: [],
}
},
})
await nextTick()
const vm = wrapper.vm as any
let placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
expect(placeholder.exists()).toBeTruthy()
vm.value = ['option_1']
await nextTick()
expect(wrapper.find('.el-select-v2__tags-text').text()).toBe('a0')
placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
expect(placeholder.exists()).toBeFalsy()
vm.value = []
await nextTick()
expect(wrapper.find('.el-select-v2__tags-text').exists()).toBeFalsy()
placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
expect(placeholder.exists()).toBeTruthy()
})
})
describe('event', () => {
it('focus & blur', async () => {
const onFocus = vi.fn()
const onBlur = vi.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 = vi.fn()
const onBlur = vi.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 = vi.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 select = wrapper.findComponent(Select)
const selectVm = select.vm as any
selectVm.expanded = true
await nextTick()
await rAF()
const vm = wrapper.vm as any
const input = wrapper.find('input')
// create a new option
input.element.value = '1111'
await input.trigger('input')
await nextTick()
expect(selectVm.filteredOptions.length).toBe(1)
// selected the new option
selectVm.onSelect(selectVm.filteredOptions[0])
expect(vm.value).toBe('1111')
selectVm.expanded = false
await nextTick()
await rAF()
selectVm.expanded = true
await nextTick()
await rAF()
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')
const input = wrapper.find('input')
input.element.value = '1111'
await input.trigger('input')
await nextTick()
const select = wrapper.findComponent(Select)
const selectVm = select.vm as any
expect(selectVm.filteredOptions.length).toBe(1)
// selected the new option
selectVm.onSelect(selectVm.filteredOptions[0])
// closed the menu
await wrapper.trigger('click')
input.element.value = '2222'
await input.trigger('input')
await nextTick()
selectVm.onSelect(selectVm.filteredOptions[0])
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('reserve-keyword', async () => {
const wrapper = createSelect({
data: () => {
return {
filterable: true,
clearable: true,
multiple: true,
reserveKeyword: true,
options: [
{
value: 'a1',
label: 'a1',
},
{
value: 'b1',
label: 'b1',
},
{
value: 'a2',
label: 'a2',
},
{
value: 'b2',
label: 'b2',
},
],
}
},
})
await nextTick()
const vm = wrapper.vm as any
await nextTick()
await wrapper.trigger('click')
const input = wrapper.find('input')
input.element.value = 'a'
await input.trigger('input')
await nextTick()
let options = getOptions()
expect(options.length).toBe(2)
options[0].click()
await nextTick()
options = getOptions()
expect(options.length).toBe(2)
input.element.value = ''
await input.trigger('input')
await nextTick()
options = getOptions()
expect(options.length).toBe(4)
vm.reserveKeyword = false
await nextTick()
input.element.value = 'a'
await input.trigger('input')
await nextTick()
options = getOptions()
expect(options.length).toBe(2)
options[0].click()
await nextTick()
options = getOptions()
expect(options.length).toBe(4)
})
it('render empty slot', async () => {
const wrapper = createSelect({
data() {
return {
options: [],
}
},
slots: {
empty: '<div class="empty-slot">EmptySlot</div>',
},
})
await nextTick()
expect(
wrapper
.findComponent({
name: 'ElPopperContent',
})
.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('default value is 0', async () => {
const wrapper = createSelect({
data: () => ({
value: 0,
options: [
{
value: 0,
label: 'option_a',
},
{
value: 1,
label: 'option_b',
},
{
value: 2,
label: 'option_c',
},
],
}),
})
await nextTick()
const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
expect(placeholder.text()).toBe('option_a')
})
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({
slots: {
default: `
<div class="custom-renderer">
<span style="margin-right: 8px;">{{ item.label }}</span>
<span style="color: var(--el-text-color-secondary); font-size: 13px">
{{ item.value }}
</span>
</div>
`,
},
})
await nextTick()
expect(
wrapper
.findComponent({
name: 'ElPopperContent',
})
.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,
label: 'option 1',
disabled: true,
},
{
value: 2,
label: 'option 2',
disabled: true,
},
{
value: 3,
label: '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')
await selectInput.trigger('keydown', {
key: EVENT_CODE.backspace,
})
await nextTick()
expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe(
DEFAULT_PLACEHOLDER
)
})
describe('filter method', () => {
async function testFilterMethod({ multiple = false }) {
const filterMethod = vi.fn()
const wrapper = createSelect({
data() {
return {
filterable: true,
multiple,
}
},
methods: {
filterMethod,
},
})
const input = wrapper.find('input')
input.element.value = 'query'
await input.trigger('input')
expect(filterMethod).toHaveBeenCalled()
}
it('should call filter method', async () => {
await testFilterMethod({ multiple: false })
})
it('should call filter method in multiple mode', async () => {
await testFilterMethod({ multiple: true })
})
it('should re-render', async () => {
const wrapper = createSelect({
data() {
return {
multiple: true,
filterable: true,
}
},
methods: {
filterMethod() {
this.options = [
{
value: 1,
label: 'option 1',
},
{
value: 2,
label: 'option 2',
},
{
value: 3,
label: 'option 3',
},
]
},
},
})
const input = wrapper.find('input')
input.element.value = 'query'
await input.trigger('input')
await nextTick()
input.element.value = ''
await input.trigger('input')
await nextTick()
const options = getOptions()
expect(options.length).toBe(3)
})
})
describe('remote search', () => {
async function testRemoteSearch({ multiple = false }) {
const remoteMethod = vi.fn()
const wrapper = createSelect({
data() {
return {
filterable: true,
remote: true,
multiple,
}
},
methods: {
remoteMethod,
},
})
const input = wrapper.find('input')
input.element.value = 'query'
await input.trigger('input')
expect(remoteMethod).toHaveBeenCalled()
}
it('should call remote method', async () => {
await testRemoteSearch({ multiple: false })
})
it('should call remote method in multiple mode', async () => {
await testRemoteSearch({ multiple: true })
})
})
it('keyboard operations', async () => {
const wrapper = createSelect({
data() {
return {
multiple: true,
options: [
{
value: 1,
label: 'option 1',
disabled: true,
},
{
value: 2,
label: 'option 2',
disabled: true,
},
{
value: 3,
label: 'option 3',
},
{
value: 4,
label: 'option 4',
},
{
value: 5,
label: 'option 5',
options: [
{
value: 51,
label: 'option 5-1',
},
{
value: 52,
label: 'option 5-2',
},
{
value: 53,
label: 'option 5-3',
disabled: true,
},
],
},
{
value: 6,
label: 'option 6',
},
],
value: [],
}
},
})
const select = wrapper.findComponent(Select)
const selectVm = select.vm as any
const vm = wrapper.vm as any
await wrapper.trigger('click')
await nextTick()
expect(selectVm.states.hoveringIndex).toBe(-1)
// should skip the disabled option
selectVm.onKeyboardNavigate('forward')
selectVm.onKeyboardNavigate('forward')
await nextTick()
expect(selectVm.states.hoveringIndex).toBe(3)
// should skip the group option
selectVm.onKeyboardNavigate('backward')
selectVm.onKeyboardNavigate('backward')
selectVm.onKeyboardNavigate('backward')
selectVm.onKeyboardNavigate('backward')
await nextTick()
expect(selectVm.states.hoveringIndex).toBe(5)
selectVm.onKeyboardNavigate('backward')
selectVm.onKeyboardNavigate('backward')
selectVm.onKeyboardNavigate('backward')
await nextTick()
// navigate to the last one
expect(selectVm.states.hoveringIndex).toBe(9)
selectVm.onKeyboardSelect()
await nextTick()
expect(vm.value).toEqual([6])
})
it('multiple select when content overflow', async () => {
const wrapper = createSelect({
data() {
return {
options: [
{
value: '选项1',
label:
'黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕',
},
{
value: '选项2',
label:
'双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶',
},
{
value: '选项3',
label: '蚵仔煎蚵仔煎蚵仔煎蚵仔煎蚵仔煎蚵仔煎',
},
{
value: '选项4',
label: '龙须面',
},
{
value: '选项5',
label: '北京烤鸭',
},
],
}
},
})
const select = wrapper.findComponent(Select)
const selectVm = select.vm as any
const selectDom = wrapper.find('.el-select-v2__wrapper').element
const selectRect = {
height: 40,
width: 221,
x: 44,
y: 8,
top: 8,
}
const mockSelectWidth = vi
.spyOn(selectDom, 'getBoundingClientRect')
.mockReturnValue(selectRect as DOMRect)
selectVm.handleResize()
const options = getOptions()
options[0].click()
await nextTick()
options[1].click()
await nextTick()
options[2].click()
await nextTick()
const tagWrappers = wrapper.findAll('.el-select-v2__tags-text')
for (const tagWrapper of tagWrappers) {
const tagWrapperDom = tagWrapper.element
expect(
Number.parseInt(tagWrapperDom.style.maxWidth) === selectRect.width - 42
).toBe(true)
}
mockSelectWidth.mockRestore()
})
describe('scrollbarAlwaysOn flag control the scrollbar whether always displayed', () => {
it('The default scrollbar is not always displayed', async (done) => {
const wrapper = createSelect()
await nextTick()
const select = wrapper.findComponent(Select)
await wrapper.trigger('click')
expect((select.vm as any).expanded).toBeTruthy()
const box = document.querySelector<HTMLElement>('.el-vl__wrapper')
expect(hasClass(box, 'always-on')).toBe(false)
done()
})
it('set the scrollbar-always-on value to true, keep the scroll bar displayed', async (done) => {
const wrapper = createSelect({
data() {
return {
scrollbarAlwaysOn: true,
}
},
})
await nextTick()
const select = wrapper.findComponent(Select)
await wrapper.trigger('click')
expect((select.vm as any).expanded).toBeTruthy()
const box = document.querySelector<HTMLElement>('.el-vl__wrapper')
expect(hasClass(box, 'always-on')).toBe(true)
done()
})
})
describe('teleported API', () => {
it('should mount on popper container', async () => {
expect(document.body.innerHTML).toBe('')
createSelect({
data() {
return {
options: [
{
value: '选项1',
label:
'黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕',
},
{
value: '选项2',
label:
'双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶',
},
{
value: '选项3',
label: '蚵仔煎蚵仔煎蚵仔煎蚵仔煎蚵仔煎蚵仔煎',
},
{
value: '选项4',
label: '龙须面',
},
{
value: '选项5',
label: '北京烤鸭',
},
],
}
},
})
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR)!.innerHTML
).not.toBe('')
})
it('should not mount on the popper container', async () => {
expect(document.body.innerHTML).toBe('')
createSelect({
data() {
return {
teleported: false,
options: [
{
value: '选项1',
label:
'黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕',
},
{
value: '选项2',
label:
'双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶',
},
{
value: '选项3',
label: '蚵仔煎蚵仔煎蚵仔煎蚵仔煎蚵仔煎蚵仔煎',
},
{
value: '选项4',
label: '龙须面',
},
{
value: '选项5',
label: '北京烤鸭',
},
],
}
},
})
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
).toBe('')
})
})
})