autocomplete.test.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. import { defineComponent, nextTick, reactive } from 'vue'
  2. import { mount } from '@vue/test-utils'
  3. import { NOOP } from '@vue/shared'
  4. import { beforeEach, describe, expect, it, test, vi } from 'vitest'
  5. import { usePopperContainerId } from '@element-plus/hooks'
  6. import { ElFormItem as FormItem } from '@element-plus/components/form'
  7. import Autocomplete from '../src/autocomplete.vue'
  8. vi.unmock('lodash')
  9. vi.useFakeTimers()
  10. const _mount = (
  11. payload = {},
  12. type: 'fn-cb' | 'fn-promise' | 'fn-arr' | 'fn-async' | 'arr' = 'fn-cb'
  13. ) =>
  14. mount(
  15. defineComponent({
  16. setup(_, { expose }) {
  17. const state = reactive({
  18. value: '',
  19. list: [
  20. { value: 'Java', tag: 'java' },
  21. { value: 'Go', tag: 'go' },
  22. { value: 'JavaScript', tag: 'javascript' },
  23. { value: 'Python', tag: 'python' },
  24. ],
  25. payload,
  26. })
  27. function filterList(queryString: string) {
  28. return queryString
  29. ? state.list.filter(
  30. (i) => i.value.indexOf(queryString.toLowerCase()) === 0
  31. )
  32. : state.list
  33. }
  34. const querySearch = (() => {
  35. switch (type) {
  36. case 'fn-cb':
  37. return (
  38. queryString: string,
  39. cb: (arg: typeof state.list) => void
  40. ) => {
  41. cb(filterList(queryString))
  42. }
  43. case 'fn-promise':
  44. return (queryString: string) =>
  45. Promise.resolve(filterList(queryString))
  46. case 'fn-async':
  47. return async (queryString: string) => {
  48. await Promise.resolve()
  49. return filterList(queryString)
  50. }
  51. case 'fn-arr':
  52. return (queryString: string) => filterList(queryString)
  53. case 'arr':
  54. return state.list
  55. }
  56. })()
  57. const containerExposes = usePopperContainerId()
  58. expose(containerExposes)
  59. return () => (
  60. <Autocomplete
  61. ref="autocomplete"
  62. v-model={state.value}
  63. fetch-suggestions={querySearch}
  64. {...state.payload}
  65. />
  66. )
  67. },
  68. }),
  69. {
  70. global: {
  71. provide: {
  72. namespace: 'el',
  73. },
  74. },
  75. }
  76. )
  77. describe('Autocomplete.vue', () => {
  78. beforeEach(() => {
  79. document.body.innerHTML = ''
  80. })
  81. test('placeholder', async () => {
  82. const wrapper = _mount()
  83. await nextTick()
  84. await wrapper.setProps({ placeholder: 'autocomplete' })
  85. expect(wrapper.find('input').attributes('placeholder')).toBe('autocomplete')
  86. await wrapper.setProps({ placeholder: 'placeholder' })
  87. expect(wrapper.find('input').attributes('placeholder')).toBe('placeholder')
  88. })
  89. test('triggerOnFocus', async () => {
  90. const fetchSuggestions = vi.fn()
  91. const wrapper = _mount({
  92. debounce: 10,
  93. fetchSuggestions,
  94. })
  95. await nextTick()
  96. await wrapper.setProps({ triggerOnFocus: false })
  97. await wrapper.find('input').trigger('focus')
  98. vi.runAllTimers()
  99. await nextTick()
  100. expect(fetchSuggestions).toHaveBeenCalledTimes(0)
  101. await wrapper.find('input').trigger('blur')
  102. await wrapper.setProps({ triggerOnFocus: true })
  103. await wrapper.find('input').trigger('focus')
  104. vi.runAllTimers()
  105. await nextTick()
  106. expect(fetchSuggestions).toHaveBeenCalledTimes(1)
  107. })
  108. test('popperClass', async () => {
  109. const wrapper = _mount()
  110. await nextTick()
  111. await wrapper.setProps({ popperClass: 'error' })
  112. expect(
  113. document.body.querySelector('.el-popper')?.classList.contains('error')
  114. ).toBe(true)
  115. await wrapper.setProps({ popperClass: 'success' })
  116. expect(
  117. document.body.querySelector('.el-popper')?.classList.contains('error')
  118. ).toBe(false)
  119. expect(
  120. document.body.querySelector('.el-popper')?.classList.contains('success')
  121. ).toBe(true)
  122. })
  123. test('teleported', async () => {
  124. _mount({ teleported: false })
  125. expect(document.body.querySelector('.el-popper__mask')).toBeNull()
  126. })
  127. test('debounce / fetchSuggestions', async () => {
  128. const fetchSuggestions = vi.fn()
  129. const wrapper = _mount({
  130. debounce: 10,
  131. fetchSuggestions,
  132. })
  133. await nextTick()
  134. await wrapper.find('input').trigger('focus')
  135. await wrapper.find('input').trigger('blur')
  136. await wrapper.find('input').trigger('focus')
  137. await wrapper.find('input').trigger('blur')
  138. await wrapper.find('input').trigger('focus')
  139. await wrapper.find('input').trigger('blur')
  140. expect(fetchSuggestions).toHaveBeenCalledTimes(0)
  141. vi.runAllTimers()
  142. await nextTick()
  143. expect(fetchSuggestions).toHaveBeenCalledTimes(1)
  144. await wrapper.find('input').trigger('focus')
  145. vi.runAllTimers()
  146. await nextTick()
  147. expect(fetchSuggestions).toHaveBeenCalledTimes(2)
  148. })
  149. test('fetchSuggestions with fn-promise', async () => {
  150. const wrapper = _mount({ debounce: 10 }, 'fn-promise')
  151. await nextTick()
  152. await wrapper.find('input').trigger('focus')
  153. vi.runAllTimers()
  154. await nextTick()
  155. const target = wrapper.getComponent(Autocomplete).vm as InstanceType<
  156. typeof Autocomplete
  157. >
  158. expect(target.suggestions.length).toBe(4)
  159. })
  160. test('fetchSuggestions with fn-async', async () => {
  161. const wrapper = _mount({ debounce: 10 }, 'fn-async')
  162. await nextTick()
  163. await wrapper.find('input').trigger('focus')
  164. vi.runAllTimers()
  165. await nextTick()
  166. await nextTick()
  167. const target = wrapper.getComponent(Autocomplete).vm as InstanceType<
  168. typeof Autocomplete
  169. >
  170. expect(target.suggestions.length).toBe(4)
  171. })
  172. test('fetchSuggestions with fn-arr', async () => {
  173. const wrapper = _mount({ debounce: 10 }, 'fn-arr')
  174. await nextTick()
  175. await wrapper.find('input').trigger('focus')
  176. vi.runAllTimers()
  177. await nextTick()
  178. const target = wrapper.getComponent(Autocomplete).vm as InstanceType<
  179. typeof Autocomplete
  180. >
  181. expect(target.suggestions.length).toBe(4)
  182. })
  183. test('fetchSuggestions with arr', async () => {
  184. const wrapper = _mount({ debounce: 10 }, 'arr')
  185. await nextTick()
  186. await wrapper.find('input').trigger('focus')
  187. vi.runAllTimers()
  188. await nextTick()
  189. const target = wrapper.getComponent(Autocomplete).vm as InstanceType<
  190. typeof Autocomplete
  191. >
  192. expect(target.suggestions.length).toBe(4)
  193. })
  194. test('valueKey / modelValue', async () => {
  195. const wrapper = _mount()
  196. await nextTick()
  197. const target = wrapper.getComponent(Autocomplete).vm as InstanceType<
  198. typeof Autocomplete
  199. >
  200. await target.handleSelect({ value: 'Go', tag: 'go' })
  201. expect(target.modelValue).toBe('Go')
  202. await wrapper.setProps({ valueKey: 'tag' })
  203. await target.handleSelect({ value: 'Go', tag: 'go' })
  204. expect(target.modelValue).toBe('go')
  205. })
  206. test('hideLoading', async () => {
  207. const wrapper = _mount({
  208. hideLoading: false,
  209. fetchSuggestions: NOOP,
  210. debounce: 10,
  211. })
  212. await nextTick()
  213. await wrapper.find('input').trigger('focus')
  214. vi.runAllTimers()
  215. await nextTick()
  216. expect(document.body.querySelector('.el-icon-loading')).toBeDefined()
  217. await wrapper.setProps({ hideLoading: true })
  218. expect(document.body.querySelector('.el-icon-loading')).toBeNull()
  219. })
  220. test('selectWhenUnmatched', async () => {
  221. const wrapper = _mount({
  222. selectWhenUnmatched: true,
  223. debounce: 10,
  224. })
  225. await nextTick()
  226. const target = wrapper.getComponent(Autocomplete).vm as InstanceType<
  227. typeof Autocomplete
  228. >
  229. target.highlightedIndex = 0
  230. target.handleKeyEnter()
  231. vi.runAllTimers()
  232. await nextTick()
  233. expect(target.highlightedIndex).toBe(-1)
  234. })
  235. test('highlightFirstItem', async () => {
  236. const wrapper = _mount({
  237. highlightFirstItem: false,
  238. debounce: 10,
  239. })
  240. await nextTick()
  241. await wrapper.find('input').trigger('focus')
  242. vi.runAllTimers()
  243. await nextTick()
  244. expect(document.body.querySelector('.highlighted')).toBeNull()
  245. await wrapper.setProps({ highlightFirstItem: true })
  246. await wrapper.find('input').trigger('focus')
  247. vi.runAllTimers()
  248. await nextTick()
  249. expect(document.body.querySelector('.highlighted')).toBeDefined()
  250. })
  251. test('fitInputWidth', async () => {
  252. const wrapper = _mount({
  253. fitInputWidth: true,
  254. })
  255. await nextTick()
  256. const inputDom = wrapper.find('.el-input').element
  257. const mockInputWidth = vi
  258. .spyOn(inputDom as HTMLElement, 'offsetWidth', 'get')
  259. .mockReturnValue(200)
  260. await wrapper.find('input').trigger('focus')
  261. vi.runAllTimers()
  262. await nextTick()
  263. await nextTick()
  264. await nextTick()
  265. expect(
  266. (
  267. document.body.querySelector(
  268. '.el-autocomplete-suggestion'
  269. ) as HTMLElement
  270. ).style.width
  271. ).toBe('200px')
  272. mockInputWidth.mockRestore()
  273. })
  274. describe('teleported API', () => {
  275. it('should mount on popper container', async () => {
  276. expect(document.body.innerHTML).toBe('')
  277. const { vm } = _mount()
  278. await nextTick()
  279. const { selector } = vm
  280. expect(document.body.querySelector(selector)?.innerHTML).not.toBe('')
  281. })
  282. it('should not mount on the popper container', async () => {
  283. expect(document.body.innerHTML).toBe('')
  284. const { vm } = _mount({
  285. teleported: false,
  286. })
  287. await nextTick()
  288. const { selector } = vm
  289. expect(document.body.querySelector(selector)?.innerHTML).toBe('')
  290. })
  291. })
  292. describe('form item accessibility integration', () => {
  293. test('automatic id attachment', async () => {
  294. const wrapper = mount(() => (
  295. <FormItem label="Foobar" data-test-ref="item">
  296. <Autocomplete data-test-ref="input" />
  297. </FormItem>
  298. ))
  299. await nextTick()
  300. const formItem = wrapper.find('[data-test-ref="item"]')
  301. const input = await wrapper.find('[data-test-ref="input"]')
  302. const formItemLabel = formItem.find('.el-form-item__label')
  303. expect(formItem.attributes().role).toBeFalsy()
  304. expect(formItemLabel.attributes().for).toBe(input.attributes().id)
  305. })
  306. test('specified id attachment', async () => {
  307. const wrapper = mount(() => (
  308. <FormItem label="Foobar" data-test-ref="item">
  309. <Autocomplete id="foobar" data-test-ref="input" />
  310. </FormItem>
  311. ))
  312. await nextTick()
  313. const formItem = wrapper.find('[data-test-ref="item"]')
  314. const input = await wrapper.find('[data-test-ref="input"]')
  315. const formItemLabel = formItem.find('.el-form-item__label')
  316. expect(formItem.attributes().role).toBeFalsy()
  317. expect(input.attributes().id).toBe('foobar')
  318. expect(formItemLabel.attributes().for).toBe(input.attributes().id)
  319. })
  320. test('form item role is group when multiple autocompletes', async () => {
  321. const wrapper = mount(() => (
  322. <FormItem label="Foobar" data-test-ref="item">
  323. <Autocomplete data-test-ref="input1" />
  324. <Autocomplete data-test-ref="input2" />
  325. </FormItem>
  326. ))
  327. await nextTick()
  328. const formItem = wrapper.find('[data-test-ref="item"]')
  329. expect(formItem.attributes().role).toBe('group')
  330. })
  331. })
  332. test('event:focus', async () => {
  333. const onFocus = vi.fn()
  334. const wrapper = _mount({ onFocus })
  335. await nextTick()
  336. const target = wrapper.getComponent(Autocomplete).vm as InstanceType<
  337. typeof Autocomplete
  338. >
  339. await wrapper.find('input').trigger('focus')
  340. vi.runAllTimers()
  341. await nextTick()
  342. expect(onFocus).toHaveBeenCalledTimes(1)
  343. await target.handleSelect({ value: 'Go', tag: 'go' })
  344. expect(target.modelValue).toBe('Go')
  345. vi.runAllTimers()
  346. await nextTick()
  347. expect(onFocus).toHaveBeenCalledTimes(1)
  348. await wrapper.find('input').trigger('blur')
  349. vi.runAllTimers()
  350. await nextTick()
  351. expect(onFocus).toHaveBeenCalledTimes(1)
  352. })
  353. test('event:blur', async () => {
  354. const onBlur = vi.fn()
  355. const wrapper = _mount({ onBlur })
  356. await nextTick()
  357. const target = wrapper.getComponent(Autocomplete).vm as InstanceType<
  358. typeof Autocomplete
  359. >
  360. await wrapper.find('input').trigger('focus')
  361. await target.handleSelect({ value: 'Go', tag: 'go' })
  362. expect(target.modelValue).toBe('Go')
  363. expect(onBlur).toHaveBeenCalledTimes(0)
  364. await wrapper.find('input').trigger('blur')
  365. vi.runAllTimers()
  366. await nextTick()
  367. expect(onBlur).toHaveBeenCalledTimes(1)
  368. })
  369. })