import { nextTick } from 'vue' import { mount } from '@vue/test-utils' import CascaderPanel from '../src/index.vue' const NORMAL_OPTIONS = [ { value: 'beijing', label: 'Beijing', children: [], leaf: true, }, { value: 'zhejiang', label: 'Zhejiang', children: [ { value: 'hangzhou', label: 'Hangzhou', }, { value: 'ningbo', label: 'Ningbo', }, ], }, { value: 'shanghai', label: 'Shanghai', children: [ // for test nodes in different levels have same value { value: 'shanghai', label: 'Shanghai', }, ], }, ] const DISABLED_OPTIONS = [ { value: 'beijing', label: 'beijing', disabled: true, }, { value: 'zhejiang', label: 'Zhejiang', children: [ { value: 'hangzhou', label: 'Hangzhou', }, ], }, { value: 'jiangsu', label: 'Jiangsu', disabled: true, children: [ { value: 'nanjing', label: 'Nanjing', }, ], }, ] const CUSTOM_PROPS_OPTIONS = [ { id: 'beijing', name: 'Beijing', areas: [], }, { id: 'zhejiang', name: 'Zhejiang', areas: [ { id: 'hangzhou', name: 'Hangzhou', }, { id: 'ningbo', name: 'Ningbo', invalid: true, }, ], }, ] const MENU = '.el-cascader-menu' const NODE = '.el-cascader-node' const VALID_NODE = '.el-cascader-node:not(.is-disabled)' const EXPAND_ARROW = '.el-icon-arrow-right.el-cascader-node__postfix' const CHECKBOX = '.el-checkbox__input' const RADIO = '.el-radio__input' let id = 0 const _mount: typeof mount = (options) => mount({ components: { CascaderPanel, }, ...options, }) const lazyLoad = (node, resolve) => { const { level } = node setTimeout(() => { const nodes = Array.from({ length: level + 1 }).map(() => ({ value: ++id, label: `option${id}`, leaf: level >= 1, })) resolve(nodes) }, 1000) } beforeEach(() => { id = 0 }) jest.useFakeTimers() describe('CascaderPanel.vue', () => { test('expand and check', async () => { const handleChange = jest.fn() const handleExpandChange = jest.fn() const wrapper = _mount({ template: ` `, data() { return { options: NORMAL_OPTIONS, value: [], } }, methods: { handleChange, handleExpandChange, }, }) const options = wrapper.findAll(NODE) const [bjNode, zjNode] = options expect(wrapper.findAll(MENU).length).toBe(1) expect(options.length).toBe(3) expect(bjNode.text()).toBe('Beijing') expect(bjNode.find(EXPAND_ARROW).exists()).toBe(false) await zjNode.trigger('click') const menus = wrapper.findAll(MENU) const hzNode = menus[1].find(NODE) expect(menus.length).toBe(2) expect(handleExpandChange).toBeCalledTimes(1) await hzNode.trigger('click') // won't trigger when expanding node not change expect(handleExpandChange).toBeCalledTimes(1) expect(handleChange).toBeCalledTimes(1) expect(wrapper.vm.value).toEqual(['zhejiang', 'hangzhou']) await bjNode.trigger('click') expect(wrapper.findAll(MENU).length).toBe(1) expect(handleExpandChange).toBeCalledTimes(2) expect(handleChange).toBeCalledTimes(2) expect(wrapper.vm.value).toEqual(['beijing']) }) test('with default value', async () => { const wrapper = mount(CascaderPanel, { props: { modelValue: ['zhejiang', 'hangzhou'], options: NORMAL_OPTIONS, }, }) await nextTick() const menus = wrapper.findAll(MENU) const [, zjNode, shNode] = menus[0].findAll(NODE) const hzNode = menus[1].find(NODE) expect(menus.length).toBe(2) expect(zjNode.classes('in-active-path')).toBe(true) expect(hzNode.classes('is-active')).toBe(true) expect(hzNode.find('.el-icon-check').exists()).toBe(true) await wrapper.setProps({ modelValue: ['beijing'] }) expect(wrapper.findAll(MENU).length).toBe(1) expect(wrapper.find(NODE).classes('is-active')).toBe(true) // leaf node should be checked await wrapper.setProps({ modelValue: ['shanghai', 'shanghai'] }) const secondMenu = wrapper.findAll(MENU)[1] expect(shNode.classes('is-active')).toBe(false) expect(secondMenu.find(NODE).classes('is-active')).toBe(true) // leaf node should be checked await wrapper.setProps({ modelValue: null }) expect(wrapper.find('is-active').exists()).toBe(false) }) test('disabled options', async () => { const handleChange = jest.fn() const handleExpandChange = jest.fn() const wrapper = _mount({ template: ` `, data() { return { options: DISABLED_OPTIONS, value: [], } }, methods: { handleChange, handleExpandChange, }, }) const [bjNode, zjNode, jsNode] = wrapper.findAll(NODE) expect(wrapper.findAll(VALID_NODE).length).toBe(1) await bjNode.trigger('click') expect(handleChange).not.toBeCalled() expect(handleExpandChange).not.toBeCalled() await jsNode.trigger('click') expect(handleExpandChange).not.toBeCalled() await zjNode.trigger('click') expect(wrapper.findAll(MENU).length).toBe(2) expect(handleExpandChange).toBeCalledTimes(1) }) test('options change', async () => { const wrapper = mount(CascaderPanel, { props: { options: NORMAL_OPTIONS, }, }) expect(wrapper.find(NODE).exists()).toBe(true) await wrapper.setProps({ options: null }) expect(wrapper.find(NODE).exists()).toBe(false) }) test('expand by hover', async () => { const wrapper = mount(CascaderPanel, { props: { options: DISABLED_OPTIONS, props: { expandTrigger: 'hover', }, }, }) const [zjNode, jsNode] = wrapper.findAll(NODE).slice(1) await jsNode.trigger('mouseenter') expect(wrapper.findAll(MENU).length).toBe(1) await zjNode.trigger('mouseenter') expect(wrapper.findAll(MENU).length).toBe(2) }) test('emit value only', async () => { const wrapper = _mount({ template: ` `, data() { return { options: NORMAL_OPTIONS, props: { emitPath: false }, value: 'shanghai', } }, }) await nextTick() const shNode = wrapper.findAll(MENU)[1].find(NODE) expect(shNode.classes('is-active')).toBe(true) await wrapper.find(NODE).trigger('click') expect(wrapper.vm.value).toBe('beijing') }) test('emit value only, issue 1531', async () => { const wrapper = _mount({ template: ` `, data() { return { options: [ { value: 0, label: 'label one', }, { value: 1, label: 'label two', }, ], props: { emitPath: false }, value: null, } }, }) await nextTick() const shNode = wrapper.findAll(MENU)[0].find(NODE) expect(shNode.classes('is-active')).toBe(false) await wrapper.findAll(NODE)[0].trigger('click') expect(wrapper.vm.value).toBe(0) await wrapper.findAll(NODE)[1].trigger('click') expect(wrapper.vm.value).toBe(1) }) test('multiple mode', async () => { const wrapper = _mount({ template: ` `, data() { return { options: NORMAL_OPTIONS, props: { multiple: true }, value: [], } }, }) const zjNode = wrapper.findAll(NODE)[1] const zjCheckbox = zjNode.find(CHECKBOX) expect(zjCheckbox.exists()).toBe(true) await zjNode.trigger('click') const secondMenu = wrapper.findAll(MENU)[1] const [hzCheckbox, nbCheckbox] = secondMenu.findAll(CHECKBOX) await hzCheckbox.find('input').trigger('click') expect(hzCheckbox.classes('is-checked')).toBe(true) expect(zjCheckbox.classes('is-indeterminate')).toBe(true) expect(wrapper.vm.value).toEqual([['zhejiang', 'hangzhou']]) await nbCheckbox.find('input').trigger('click') expect(zjCheckbox.classes('is-checked')).toBe(true) expect(wrapper.vm.value).toEqual([ ['zhejiang', 'hangzhou'], ['zhejiang', 'ningbo'], ]) await zjCheckbox.find('input').trigger('click') expect(zjCheckbox.classes('is-checked')).toBe(false) expect(nbCheckbox.classes('is-checked')).toBe(false) expect(nbCheckbox.classes('is-checked')).toBe(false) expect(wrapper.vm.value).toEqual([]) }) test('multiple mode with disabled default value', async () => { const wrapper = mount(CascaderPanel, { props: { options: DISABLED_OPTIONS, props: { multiple: true }, modelValue: [['beijing']], }, }) await nextTick() const bjNode = wrapper.find(NODE) const bjCheckbox = wrapper.find(CHECKBOX) expect(bjNode.classes('is-disabled')).toBe(true) expect(bjCheckbox.classes('is-disabled')).toBe(true) expect(bjCheckbox.classes('is-checked')).toBe(true) }) test('check strictly in single mode', async () => { const wrapper = _mount({ template: ` `, data() { return { options: NORMAL_OPTIONS, props: { checkStrictly: true }, value: [], } }, }) const zjRadio = wrapper.findAll(RADIO)[1] expect(zjRadio.exists()).toBe(true) await zjRadio.find('input').trigger('click') expect(wrapper.vm.value).toEqual(['zhejiang']) }) test('check strictly in single mode with disabled options', async () => { const wrapper = mount(CascaderPanel, { props: { options: DISABLED_OPTIONS, props: { checkStrictly: true }, }, }) const [bjNode, , jsNode] = wrapper.findAll(NODE) const bjRadio = bjNode.find(RADIO) // leaf nodes should add a disabled style for that they are not expandable // but non-leaf nodes are not expect(bjNode.classes('is-disabled')).toBe(true) expect(jsNode.classes('is-disabled')).toBe(false) expect(bjRadio.classes('is-disabled')).toBe(true) expect(bjRadio.find('input[disabled]').exists()).toBe(true) await jsNode.trigger('click') expect(wrapper.findAll(MENU).length).toEqual(2) }) test('check strictly in multiple mode', async () => { const wrapper = _mount({ template: ` `, data() { return { options: NORMAL_OPTIONS, props: { checkStrictly: true, multiple: true }, value: [['shanghai']], } }, }) const shNode = wrapper.findAll(NODE)[2] const [, zjCheckbox, shCheckbox] = wrapper.findAll(CHECKBOX) await nextTick() await shNode.trigger('click') const shCheckbox2 = wrapper.findAll(MENU)[1].find(CHECKBOX) expect(shCheckbox.classes('is-checked')).toBe(true) expect(shCheckbox2.classes('is-checked')).toBe(false) await zjCheckbox.find('input').trigger('click') expect(wrapper.vm.value).toEqual([['shanghai'], ['zhejiang']]) }) test('custom props', async () => { const wrapper = _mount({ template: ` `, data() { return { options: CUSTOM_PROPS_OPTIONS, props: { value: 'id', label: 'name', children: 'areas', disabled: 'invalid', leaf: (data: typeof CUSTOM_PROPS_OPTIONS[0]) => !data.areas?.length, }, value: [], } }, }) const [bjNode, zjNode] = wrapper.findAll(NODE) expect(bjNode.text()).toBe('Beijing') expect(bjNode.find(EXPAND_ARROW).exists()).toBe(false) await zjNode.trigger('click') const [hzNode, nbNode] = wrapper.findAll(MENU)[1].findAll(NODE) expect(hzNode.exists()).toBe(true) expect(nbNode.classes('is-disabled')).toBe(true) await hzNode.trigger('click') expect(wrapper.vm.value).toEqual(['zhejiang', 'hangzhou']) }) test('lazy load', async () => { const wrapper = _mount({ template: ` `, data() { return { value: [], props: { lazy: true, lazyLoad, }, } }, }) jest.runAllTimers() await nextTick() const firstOption = wrapper.find(NODE) expect(firstOption.exists()).toBe(true) await firstOption.trigger('click') expect(firstOption.find('.el-icon-loading').exists()).toBe(true) jest.runAllTimers() await nextTick() expect(firstOption.find('.el-icon-loading').exists()).toBe(false) const secondMenu = wrapper.findAll(MENU)[1] expect(secondMenu.exists()).toBe(true) await secondMenu.find(NODE).trigger('click') expect(wrapper.vm.value).toEqual([1, 2]) }) test('lazy load with default primitive value', async () => { const wrapper = mount(CascaderPanel, { props: { props: { lazy: true, lazyLoad, }, modelValue: [1, 2], }, }) jest.runAllTimers() await nextTick() expect(wrapper.findAll(MENU).length).toBe(2) expect(wrapper.find(`.is-active`).text()).toBe('option2') }) test('lazy load with default object value', async () => { const wrapper = mount(CascaderPanel, { props: { props: { lazy: true, lazyLoad(node, resolve) { const { level } = node setTimeout(() => { const nodes = Array.from({ length: level + 1 }).map(() => ({ value: { id: ++id }, label: `option${id}`, leaf: level >= 1, })) resolve(nodes) }, 1000) }, }, modelValue: [{ id: 1 }, { id: 2 }], }, }) jest.runAllTimers() await nextTick() expect(wrapper.findAll(MENU).length).toBe(2) expect(wrapper.find(`.is-active`).text()).toBe('option2') }) test('no loaded nodes should not be checked', async () => { const wrapper = _mount({ template: ` `, data() { return { props: { multiple: true, lazy: true, lazyLoad(node, resolve) { const { level } = node setTimeout(() => { const nodes = Array.from({ length: level + 1 }).map(() => { ++id return { value: id, label: `option${id}`, leaf: id === 3, } }) resolve(nodes) }, 1000) }, }, } }, }) jest.runAllTimers() await nextTick() const firstMenu = wrapper.findAll(MENU)[0] const firstOption = wrapper.find(NODE) await firstOption.trigger('click') jest.runAllTimers() await nextTick() await firstMenu.find(CHECKBOX).find('input').trigger('click') const secondMenu = wrapper.findAll(MENU)[1] expect(secondMenu.exists()).toBe(true) expect(firstMenu.find(CHECKBOX).classes('is-checked')).toBe(false) expect(firstMenu.find(CHECKBOX).classes('is-indeterminate')).toBe(true) expect(secondMenu.findAll(CHECKBOX)[0].classes('is-checked')).toBe(false) expect(secondMenu.findAll(CHECKBOX)[0].classes('is-indeterminate')).toBe( false ) expect(secondMenu.findAll(CHECKBOX)[1].classes('is-checked')).toBe(true) expect(secondMenu.findAll(CHECKBOX)[1].classes('is-indeterminate')).toBe( false ) }) test('getCheckedNodes and clearCheckedNodes', () => { const wrapper = mount(CascaderPanel, { props: { options: NORMAL_OPTIONS, props: { multiple: true }, modelValue: [['shanghai', 'shanghai']], }, }) const vm = wrapper.vm as any expect(vm.getCheckedNodes().length).toBe(2) expect(vm.getCheckedNodes(true).length).toBe(1) vm.clearCheckedNodes() expect(vm.getCheckedNodes().length).toBe(0) }) test('options async load with default value', async () => { const wrapper = mount(CascaderPanel, { props: { options: [], modelValue: ['shanghai', 'shanghai'], }, }) const vm = wrapper.vm as any expect(vm.getCheckedNodes().length).toBe(0) await wrapper.setProps({ options: NORMAL_OPTIONS }) expect(vm.getCheckedNodes(true).length).toBe(1) }) })