cascader.test.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. import { nextTick, reactive, ref } from 'vue'
  2. import { mount } from '@vue/test-utils'
  3. import { afterEach, describe, expect, it, test, vi } from 'vitest'
  4. import { EVENT_CODE } from '@element-plus/constants'
  5. import triggerEvent from '@element-plus/test-utils/trigger-event'
  6. import { ArrowDown, Check, CircleClose } from '@element-plus/icons-vue'
  7. import { usePopperContainerId } from '@element-plus/hooks'
  8. import { hasClass } from '@element-plus/utils'
  9. import ElForm, { ElFormItem } from '@element-plus/components/form'
  10. import Cascader from '../src/cascader.vue'
  11. import type { VNode } from 'vue'
  12. vi.mock('lodash-unified', async () => {
  13. return {
  14. ...((await vi.importActual('lodash-unified')) as Record<string, any>),
  15. debounce: vi.fn((fn) => {
  16. fn.cancel = vi.fn()
  17. fn.flush = vi.fn()
  18. return fn
  19. }),
  20. }
  21. })
  22. const OPTIONS = [
  23. {
  24. value: 'zhejiang',
  25. label: 'Zhejiang',
  26. children: [
  27. {
  28. value: 'hangzhou',
  29. label: 'Hangzhou',
  30. },
  31. {
  32. value: 'ningbo',
  33. label: 'Ningbo',
  34. },
  35. {
  36. value: 'wenzhou',
  37. label: 'Wenzhou',
  38. },
  39. ],
  40. },
  41. ]
  42. const AXIOM = 'Rem is the best girl'
  43. const TRIGGER = '.el-cascader'
  44. const NODE = '.el-cascader-node'
  45. const TAG = '.el-tag'
  46. const SUGGESTION_ITEM = '.el-cascader__suggestion-item'
  47. const SUGGESTION_PANEL = '.el-cascader__suggestion-panel'
  48. const DROPDOWN = '.el-cascader__dropdown'
  49. const _mount = (render: () => VNode) =>
  50. mount(render, {
  51. attachTo: document.body,
  52. })
  53. afterEach(() => {
  54. document.body.innerHTML = ''
  55. })
  56. describe('Cascader.vue', () => {
  57. test('toggle popper visible', async () => {
  58. const handleVisibleChange = vi.fn()
  59. const wrapper = _mount(() => (
  60. <Cascader onVisibleChange={handleVisibleChange} />
  61. ))
  62. const trigger = wrapper.find(TRIGGER)
  63. const dropdown = wrapper.findComponent(ArrowDown).element as HTMLDivElement
  64. await trigger.trigger('click')
  65. expect(dropdown.style.display).not.toBe('none')
  66. expect(handleVisibleChange).toBeCalledWith(true)
  67. await trigger.trigger('click')
  68. expect(handleVisibleChange).toBeCalledWith(false)
  69. await trigger.trigger('click')
  70. document.body.click()
  71. expect(handleVisibleChange).toBeCalledWith(false)
  72. })
  73. test('expand and check', async () => {
  74. const handleChange = vi.fn()
  75. const handleExpandChange = vi.fn()
  76. const value = ref([])
  77. const wrapper = _mount(() => (
  78. <Cascader
  79. v-model={value.value}
  80. options={OPTIONS}
  81. onChange={handleChange}
  82. onExpandChange={handleExpandChange}
  83. />
  84. ))
  85. const trigger = wrapper.find(TRIGGER)
  86. await trigger.trigger('click')
  87. ;(document.querySelector(NODE) as HTMLElement).click()
  88. await nextTick()
  89. expect(handleExpandChange).toBeCalledWith(['zhejiang'])
  90. ;(document.querySelectorAll(NODE)[1] as HTMLElement).click()
  91. await nextTick()
  92. expect(handleChange).toBeCalledWith(['zhejiang', 'hangzhou'])
  93. expect(value.value).toEqual(['zhejiang', 'hangzhou'])
  94. expect(wrapper.find('input').element.value).toBe('Zhejiang / Hangzhou')
  95. })
  96. test('with default value', async () => {
  97. const value = ref(['zhejiang', 'hangzhou'])
  98. const wrapper = _mount(() => (
  99. <Cascader v-model={value.value} options={OPTIONS} />
  100. ))
  101. await nextTick()
  102. expect(wrapper.find('input').element.value).toBe('Zhejiang / Hangzhou')
  103. value.value = ['zhejiang', 'ningbo']
  104. await nextTick()
  105. expect(wrapper.find('input').element.value).toBe('Zhejiang / Ningbo')
  106. })
  107. test('options change', async () => {
  108. const value = ref(['zhejiang', 'hangzhou'])
  109. const options = ref(OPTIONS)
  110. const wrapper = _mount(() => (
  111. <Cascader v-model={value.value} options={options.value} />
  112. ))
  113. options.value = []
  114. await nextTick()
  115. expect(wrapper.find('input').element.value).toBe('')
  116. })
  117. test('disabled', async () => {
  118. const handleVisibleChange = vi.fn()
  119. const wrapper = _mount(() => (
  120. <Cascader disabled onVisibleChange={handleVisibleChange} />
  121. ))
  122. await wrapper.find(TRIGGER).trigger('click')
  123. expect(handleVisibleChange).not.toBeCalled()
  124. expect(wrapper.find('input[disabled]').exists()).toBe(true)
  125. })
  126. test('custom placeholder', async () => {
  127. const wrapper = _mount(() => <Cascader placeholder={AXIOM} />)
  128. expect(wrapper.find('input').attributes().placeholder).toBe(AXIOM)
  129. })
  130. test('clearable', async () => {
  131. const wrapper = _mount(() => (
  132. <Cascader
  133. modelValue={['zhejiang', 'hangzhou']}
  134. clearable
  135. options={OPTIONS}
  136. />
  137. ))
  138. const trigger = wrapper.find(TRIGGER)
  139. expect(wrapper.findComponent(ArrowDown).exists()).toBe(true)
  140. await trigger.trigger('mouseenter')
  141. expect(wrapper.findComponent(ArrowDown).exists()).toBe(false)
  142. await wrapper.findComponent(CircleClose).trigger('click')
  143. expect(wrapper.find('input').element.value).toBe('')
  144. expect(
  145. wrapper.findComponent(Cascader).vm.getCheckedNodes(false)?.length
  146. ).toBe(0)
  147. await trigger.trigger('mouseleave')
  148. await trigger.trigger('mouseenter')
  149. await expect(wrapper.findComponent(CircleClose).exists()).toBe(false)
  150. })
  151. test('show last level label', async () => {
  152. const wrapper = _mount(() => (
  153. <Cascader
  154. modelValue={['zhejiang', 'hangzhou']}
  155. showAllLevels={false}
  156. options={OPTIONS}
  157. />
  158. ))
  159. await nextTick()
  160. expect(wrapper.find('input').element.value).toBe('Hangzhou')
  161. })
  162. test('multiple mode', async () => {
  163. const value = ref([
  164. ['zhejiang', 'hangzhou'],
  165. ['zhejiang', 'ningbo'],
  166. ])
  167. const props = { multiple: true }
  168. const wrapper = _mount(() => (
  169. <Cascader v-model={value.value} props={props} options={OPTIONS} />
  170. ))
  171. await nextTick()
  172. const tags = wrapper.findAll(TAG)
  173. const [firstTag, secondTag] = tags
  174. expect(tags.length).toBe(2)
  175. expect(firstTag.text()).toBe('Zhejiang / Hangzhou')
  176. expect(secondTag.text()).toBe('Zhejiang / Ningbo')
  177. await firstTag.find('.el-tag__close').trigger('click')
  178. expect(wrapper.findAll(TAG).length).toBe(1)
  179. expect(value.value).toEqual([['zhejiang', 'ningbo']])
  180. })
  181. test('collapse tags', async () => {
  182. const props = { multiple: true }
  183. const wrapper = _mount(() => (
  184. <Cascader
  185. modelValue={[
  186. ['zhejiang', 'hangzhou'],
  187. ['zhejiang', 'ningbo'],
  188. ['zhejiang', 'wenzhou'],
  189. ]}
  190. collapseTags
  191. props={props}
  192. options={OPTIONS}
  193. />
  194. ))
  195. await nextTick()
  196. const tags = wrapper.findAll(TAG).filter((item) => {
  197. return !hasClass(item.element, 'in-tooltip')
  198. })
  199. expect(tags[0].text()).toBe('Zhejiang / Hangzhou')
  200. expect(tags.length).toBe(2)
  201. })
  202. test('collapse tags tooltip', async () => {
  203. const props = { multiple: true }
  204. _mount(() => (
  205. <Cascader
  206. modelValue={[
  207. ['zhejiang', 'hangzhou'],
  208. ['zhejiang', 'ningbo'],
  209. ['zhejiang', 'wenzhou'],
  210. ]}
  211. collapseTags
  212. collapseTagsTooltip
  213. props={props}
  214. options={OPTIONS}
  215. />
  216. ))
  217. await nextTick()
  218. const tooltipTags = document.querySelectorAll(
  219. `.el-cascader__collapse-tags ${TAG}`
  220. )
  221. expect(tooltipTags.length).toBe(2)
  222. expect(tooltipTags[0].textContent).toBe('Zhejiang / Ningbo')
  223. expect(tooltipTags[1].textContent).toBe('Zhejiang / Wenzhou')
  224. })
  225. test('tag type', async () => {
  226. const props = { multiple: true }
  227. const wrapper = _mount(() => (
  228. <Cascader
  229. modelValue={[['zhejiang', 'hangzhou']]}
  230. tagType="success"
  231. props={props}
  232. options={OPTIONS}
  233. />
  234. ))
  235. await nextTick()
  236. expect(wrapper.find('.el-tag').classes()).toContain('el-tag--success')
  237. })
  238. test('filterable', async () => {
  239. const value = ref([])
  240. const wrapper = _mount(() => (
  241. <Cascader v-model={value.value} filterable options={OPTIONS} />
  242. ))
  243. const input = wrapper.find('input')
  244. input.element.value = 'Ni'
  245. await input.trigger('compositionstart')
  246. await input.trigger('input')
  247. input.element.value = 'Ha'
  248. await input.trigger('compositionupdate')
  249. await input.trigger('input')
  250. await input.trigger('compositionend')
  251. const suggestions = document.querySelectorAll(
  252. SUGGESTION_ITEM
  253. ) as NodeListOf<HTMLElement>
  254. const hzSuggestion = suggestions[0]
  255. expect(suggestions.length).toBe(1)
  256. expect(hzSuggestion.textContent).toBe('Zhejiang / Hangzhou')
  257. hzSuggestion.click()
  258. await nextTick()
  259. expect(wrapper.findComponent(Check).exists()).toBeTruthy()
  260. expect(value.value).toEqual(['zhejiang', 'hangzhou'])
  261. hzSuggestion.click()
  262. await nextTick()
  263. expect(value.value).toEqual(['zhejiang', 'hangzhou'])
  264. })
  265. test('filterable in multiple mode', async () => {
  266. const value = ref([])
  267. const props = { multiple: true }
  268. const wrapper = _mount(() => (
  269. <Cascader
  270. v-model={value.value}
  271. props={props}
  272. filterable
  273. options={OPTIONS}
  274. />
  275. ))
  276. const input = wrapper.find('.el-cascader__search-input')
  277. ;(input.element as HTMLInputElement).value = 'Ha'
  278. await input.trigger('input')
  279. await nextTick()
  280. const hzSuggestion = document.querySelector(SUGGESTION_ITEM) as HTMLElement
  281. hzSuggestion.click()
  282. await nextTick()
  283. expect(value.value).toEqual([['zhejiang', 'hangzhou']])
  284. hzSuggestion.click()
  285. await nextTick()
  286. expect(value.value).toEqual([])
  287. })
  288. test('filter method', async () => {
  289. const filterMethod = vi.fn((node, keyword) => {
  290. const { text, value } = node
  291. return text.includes(keyword) || value.includes(keyword)
  292. })
  293. const wrapper = _mount(() => (
  294. <Cascader filterMethod={filterMethod} filterable options={OPTIONS} />
  295. ))
  296. const input = wrapper.find('input')
  297. input.element.value = 'ha'
  298. await input.trigger('input')
  299. const hzSuggestion = document.querySelector(SUGGESTION_ITEM) as HTMLElement
  300. expect(filterMethod).toBeCalled()
  301. expect(hzSuggestion.textContent).toBe('Zhejiang / Hangzhou')
  302. })
  303. test('filterable keyboard selection', async () => {
  304. const value = ref([])
  305. const wrapper = _mount(() => (
  306. <Cascader v-model={value.value} filterable options={OPTIONS} />
  307. ))
  308. const input = wrapper.find('input')
  309. const dropdown = document.querySelector(DROPDOWN)!
  310. input.element.value = 'h'
  311. await input.trigger('input')
  312. const suggestionsPanel = document.querySelector(
  313. SUGGESTION_PANEL
  314. ) as HTMLDivElement
  315. const suggestions = dropdown.querySelectorAll(
  316. SUGGESTION_ITEM
  317. ) as NodeListOf<HTMLElement>
  318. const hzSuggestion = suggestions[0]
  319. triggerEvent(suggestionsPanel, 'keydown', EVENT_CODE.down)
  320. expect(document.activeElement!.textContent).toBe('Zhejiang / Hangzhou')
  321. triggerEvent(hzSuggestion, 'keydown', EVENT_CODE.down)
  322. expect(document.activeElement!.textContent).toBe('Zhejiang / Ningbo')
  323. triggerEvent(hzSuggestion, 'keydown', EVENT_CODE.enter)
  324. await nextTick()
  325. expect(value.value).toEqual(['zhejiang', 'hangzhou'])
  326. })
  327. describe('teleported API', () => {
  328. it('should mount on popper container', async () => {
  329. expect(document.body.innerHTML).toBe('')
  330. const value = ref([])
  331. _mount(() => (
  332. <Cascader v-model={value.value} filterable options={OPTIONS} />
  333. ))
  334. await nextTick()
  335. const { selector } = usePopperContainerId()
  336. expect(document.body.querySelector(selector.value)!.innerHTML).not.toBe(
  337. ''
  338. )
  339. })
  340. it('should not mount on the popper container', async () => {
  341. expect(document.body.innerHTML).toBe('')
  342. const value = ref([])
  343. _mount(() => (
  344. <Cascader
  345. v-model={value.value}
  346. filterable
  347. teleported={false}
  348. options={OPTIONS}
  349. />
  350. ))
  351. await nextTick()
  352. const { selector } = usePopperContainerId()
  353. expect(document.body.querySelector(selector.value)!.innerHTML).toBe('')
  354. })
  355. })
  356. test('placeholder disappear when resetForm', async () => {
  357. const model = reactive({
  358. name: new Array<string>(),
  359. })
  360. const wrapper = _mount(() => (
  361. <ElForm model={model}>
  362. <ElFormItem label="Activity name" prop="name">
  363. <Cascader
  364. v-model={model.name}
  365. options={OPTIONS}
  366. filterable
  367. placeholder={AXIOM}
  368. />
  369. </ElFormItem>
  370. </ElForm>
  371. ))
  372. model.name = ['zhejiang', 'hangzhou']
  373. await nextTick()
  374. expect(wrapper.find('input').element.placeholder).toBe('')
  375. wrapper.findComponent(ElForm).vm.$.exposed!.resetFields()
  376. await nextTick()
  377. expect(wrapper.find('input').element.placeholder).toBe(AXIOM)
  378. })
  379. test('should be able to trigger togglePopperVisible outside the component', async () => {
  380. const cascaderRef = ref()
  381. const clickFn = () => {
  382. cascaderRef.value.togglePopperVisible()
  383. }
  384. const wrapper = _mount(() => (
  385. <div>
  386. <Cascader ref="cascaderRef" options={OPTIONS} />
  387. <button onClick={clickFn} />
  388. </div>
  389. ))
  390. const dropdown = wrapper.findComponent(ArrowDown).element as HTMLDivElement
  391. expect(dropdown.style.display).not.toBe('none')
  392. const button = wrapper.find('button')
  393. await button.trigger('click')
  394. await nextTick()
  395. expect(dropdown?.style.display).not.toBe('none')
  396. })
  397. })