input.test.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. import { nextTick, ref } from 'vue'
  2. import { mount } from '@vue/test-utils'
  3. import { afterEach, describe, expect, test, vi } from 'vitest'
  4. import defineGetter from '@element-plus/test-utils/define-getter'
  5. import { ElFormItem as FormItem } from '@element-plus/components/form'
  6. import Input from '../src/input.vue'
  7. import type { CSSProperties } from 'vue'
  8. import type { InputAutoSize, InputInstance, InputProps } from '../src/input'
  9. describe('Input.vue', () => {
  10. afterEach(() => {
  11. vi.restoreAllMocks()
  12. })
  13. test('create', async () => {
  14. const input = ref('input')
  15. const handleFocus = vi.fn()
  16. const wrapper = mount(() => (
  17. <Input
  18. minlength={3}
  19. maxlength={5}
  20. placeholder="请输入内容"
  21. onFocus={handleFocus}
  22. modelValue={input.value}
  23. />
  24. ))
  25. const inputElm = wrapper.find('input')
  26. const nativeInput = inputElm.element
  27. await inputElm.trigger('focus')
  28. expect(inputElm.exists()).toBe(true)
  29. expect(handleFocus).toHaveBeenCalled()
  30. expect(nativeInput.placeholder).toMatchInlineSnapshot(`"请输入内容"`)
  31. expect(nativeInput.value).toMatchInlineSnapshot(`"input"`)
  32. expect(nativeInput.minLength).toMatchInlineSnapshot(`3`)
  33. input.value = 'text'
  34. await nextTick()
  35. expect(inputElm.element.value).toMatchInlineSnapshot(`"text"`)
  36. })
  37. test('default to empty', () => {
  38. const wrapper = mount(() => <Input />)
  39. const inputElm = wrapper.find('input')
  40. expect(inputElm.element.value).toBe('')
  41. })
  42. test('disabled', () => {
  43. const wrapper = mount(() => <Input disabled />)
  44. const inputElm = wrapper.find('input')
  45. expect(inputElm.element.disabled).not.toBeNull()
  46. })
  47. describe('test emoji', () => {
  48. test('el-input should minimize value between emoji length and maxLength', async () => {
  49. const inputVal = ref('12🌚')
  50. const wrapper = mount(() => (
  51. <Input
  52. class="test-exceed"
  53. maxlength="4"
  54. showWordLimit
  55. v-model={inputVal.value}
  56. />
  57. ))
  58. const vm = wrapper.vm
  59. const inputElm = wrapper.find('input')
  60. const nativeInput = inputElm.element
  61. expect(nativeInput.value).toMatchInlineSnapshot(`"12🌚"`)
  62. const elCount = wrapper.find('.el-input__count-inner')
  63. expect(elCount.exists()).toBe(true)
  64. expect(elCount.text()).toMatchInlineSnapshot(`"3 / 4"`)
  65. inputVal.value = '1👌3😄'
  66. await nextTick()
  67. expect(nativeInput.value).toMatchInlineSnapshot(`"1👌3😄"`)
  68. expect(elCount.text()).toMatchInlineSnapshot(`"4 / 4"`)
  69. inputVal.value = '哈哈1👌3😄'
  70. await nextTick()
  71. expect(nativeInput.value).toMatchInlineSnapshot(`"哈哈1👌3😄"`)
  72. expect(elCount.text()).toMatchInlineSnapshot(`"6 / 4"`)
  73. expect(Array.from(vm.$el.classList)).toMatchInlineSnapshot(`
  74. [
  75. "el-input",
  76. "is-exceed",
  77. "test-exceed",
  78. ]
  79. `)
  80. })
  81. test('textarea should minimize value between emoji length and maxLength', async () => {
  82. const inputVal = ref('啊好😄')
  83. const wrapper = mount(() => (
  84. <Input
  85. type="textarea"
  86. maxlength="4"
  87. showWordLimit
  88. v-model={inputVal.value}
  89. />
  90. ))
  91. const vm = wrapper.vm
  92. const inputElm = wrapper.find('textarea')
  93. const nativeInput = inputElm.element
  94. expect(nativeInput.value).toMatchInlineSnapshot(`"啊好😄"`)
  95. const elCount = wrapper.find('.el-input__count')
  96. expect(elCount.exists()).toBe(true)
  97. expect(elCount.text()).toMatchInlineSnapshot(`"3 / 4"`)
  98. inputVal.value = '哈哈1👌3😄'
  99. await nextTick()
  100. expect(nativeInput.value).toMatchInlineSnapshot(`"哈哈1👌3😄"`)
  101. expect(elCount.text()).toMatchInlineSnapshot(`"6 / 4"`)
  102. expect(Array.from(vm.$el.classList)).toMatchInlineSnapshot(`
  103. [
  104. "el-textarea",
  105. "is-exceed",
  106. ]
  107. `)
  108. })
  109. })
  110. test('suffixIcon', () => {
  111. const wrapper = mount(() => <Input suffix-icon="time" />)
  112. const icon = wrapper.find('.el-input__icon')
  113. expect(icon.exists()).toBe(true)
  114. })
  115. test('prefixIcon', () => {
  116. const wrapper = mount(() => <Input prefix-icon="time" />)
  117. const icon = wrapper.find('.el-input__icon')
  118. expect(icon.exists()).toBe(true)
  119. })
  120. test('size', () => {
  121. const wrapper = mount(() => <Input size="large" />)
  122. expect(wrapper.classes('el-input--large')).toBe(true)
  123. })
  124. test('type', () => {
  125. const wrapper = mount(() => <Input type="textarea" />)
  126. expect(wrapper.classes('el-textarea')).toBe(true)
  127. })
  128. test('rows', () => {
  129. const wrapper = mount(() => <Input type="textarea" rows={3} />)
  130. expect(wrapper.find('textarea').element.rows).toEqual(3)
  131. })
  132. test('resize', async () => {
  133. const resize = ref<InputProps['resize']>('none')
  134. const wrapper = mount(() => <Input type="textarea" resize={resize.value} />)
  135. const textarea = wrapper.find('textarea').element
  136. await nextTick()
  137. expect(textarea.style.resize).toEqual(resize.value)
  138. resize.value = 'horizontal'
  139. await nextTick()
  140. expect(textarea.style.resize).toEqual(resize.value)
  141. })
  142. test('sets value on textarea / input type change', async () => {
  143. const type = ref('text')
  144. const val = ref('123')
  145. const wrapper = mount(() => <Input type={type.value} v-model={val.value} />)
  146. const vm = wrapper.vm
  147. expect(vm.$el.querySelector('input').value).toMatchInlineSnapshot(`"123"`)
  148. type.value = 'textarea'
  149. await nextTick()
  150. await nextTick()
  151. expect(vm.$el.querySelector('textarea').value).toMatchInlineSnapshot(
  152. `"123"`
  153. )
  154. type.value = 'password'
  155. await nextTick()
  156. await nextTick()
  157. expect(vm.$el.querySelector('input').value).toMatchInlineSnapshot(`"123"`)
  158. })
  159. test('limit input and show word count', async () => {
  160. const input1 = ref('')
  161. const input2 = ref('')
  162. const input3 = ref('')
  163. const input4 = ref('exceed')
  164. const show = ref(false)
  165. const wrapper = mount(() => (
  166. <div>
  167. <Input
  168. class="test-text"
  169. type="text"
  170. v-model={input1.value}
  171. maxlength="10"
  172. showWordLimit={show.value}
  173. />
  174. <Input
  175. class="test-textarea"
  176. type="textarea"
  177. v-model={input2.value}
  178. maxlength="10"
  179. showWordLimit
  180. />
  181. <Input
  182. class="test-password"
  183. type="password"
  184. v-model={input3.value}
  185. maxlength="10"
  186. showWordLimit
  187. />
  188. <Input
  189. class="test-initial-exceed"
  190. type="text"
  191. v-model={input4.value}
  192. maxlength="2"
  193. showWordLimit
  194. />
  195. </div>
  196. ))
  197. const inputElm1 = wrapper.vm.$el.querySelector('.test-text')
  198. const inputElm2 = wrapper.vm.$el.querySelector('.test-textarea')
  199. const inputElm3 = wrapper.vm.$el.querySelector('.test-password')
  200. const inputElm4 = wrapper.vm.$el.querySelector('.test-initial-exceed')
  201. expect(inputElm1.querySelectorAll('.el-input__count').length).toEqual(0)
  202. expect(inputElm2.querySelectorAll('.el-input__count').length).toEqual(1)
  203. expect(inputElm3.querySelectorAll('.el-input__count').length).toEqual(0)
  204. expect(Array.from(inputElm4.classList)).toMatchInlineSnapshot(`
  205. [
  206. "el-input",
  207. "is-exceed",
  208. "test-initial-exceed",
  209. ]
  210. `)
  211. show.value = true
  212. await nextTick()
  213. expect(inputElm1.querySelectorAll('.el-input__count').length).toEqual(1)
  214. input4.value = '1'
  215. await nextTick()
  216. expect(Array.from(inputElm4.classList)).toMatchInlineSnapshot(`
  217. [
  218. "el-input",
  219. "test-initial-exceed",
  220. ]
  221. `)
  222. })
  223. test('use formatter and parser', () => {
  224. const val = ref('10000')
  225. const formatter = (val: string) => {
  226. return val.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
  227. }
  228. const parser = (val: string) => {
  229. return val.replace(/\$\s?|(,*)/g, '')
  230. }
  231. const wrapper = mount(() => (
  232. <Input v-model={val.value} formatter={formatter} parser={parser} />
  233. ))
  234. const vm = wrapper.vm
  235. expect(vm.$el.querySelector('input').value).toEqual('10000')
  236. expect(vm.$el.querySelector('input').value).not.toEqual('1000')
  237. })
  238. describe('Input Methods', () => {
  239. test('method:select', async () => {
  240. const testContent = ref('test')
  241. const wrapper = mount(() => <Input v-model={testContent.value} />)
  242. const input = wrapper.find('input').element
  243. // mock selectionRange behaviour, due to jsdom's reason this case cannot run well, may be fixed later using headlesschrome or puppeteer
  244. let selected = false
  245. defineGetter(input, 'selectionStart', function (this: HTMLInputElement) {
  246. return selected ? 0 : this.value.length
  247. })
  248. defineGetter(input, 'selectionEnd', function (this: HTMLInputElement) {
  249. return this.value.length
  250. })
  251. expect(input.selectionStart).toEqual(testContent.value.length)
  252. expect(input.selectionEnd).toEqual(testContent.value.length)
  253. input.select()
  254. selected = true
  255. await nextTick()
  256. expect(input.selectionStart).toEqual(0)
  257. expect(input.selectionEnd).toEqual(testContent.value.length)
  258. })
  259. test('method:resizeTextarea', async () => {
  260. const text = ref('TEXT:resizeTextarea')
  261. const wrapper = mount({
  262. setup: () => () =>
  263. (
  264. <Input
  265. ref="textarea"
  266. autosize={{ minRows: 1, maxRows: 1 }}
  267. type="textarea"
  268. v-model={text.value}
  269. />
  270. ),
  271. })
  272. const refTextarea = wrapper.vm.$refs.textarea as InputInstance
  273. const originMinHeight = (refTextarea.textareaStyle as CSSProperties)
  274. .minHeight
  275. ;(refTextarea.autosize as Exclude<InputAutoSize, boolean>).minRows = 5
  276. refTextarea.resizeTextarea()
  277. // After this textarea min-height (style) will change
  278. const nowMinHeight = (refTextarea.textareaStyle as any)[1].minHeight
  279. expect(originMinHeight).not.toEqual(nowMinHeight)
  280. })
  281. })
  282. describe('Input Events', () => {
  283. const handleFocus = vi.fn()
  284. const handleBlur = vi.fn()
  285. test('event:focus & blur', async () => {
  286. const content = ref('')
  287. const wrapper = mount(() => (
  288. <Input
  289. placeholder="请输入内容"
  290. modelValue={content.value}
  291. onFocus={handleFocus}
  292. onBlur={handleBlur}
  293. />
  294. ))
  295. const input = wrapper.find('input')
  296. await input.trigger('focus')
  297. expect(handleFocus).toBeCalled()
  298. await input.trigger('blur')
  299. expect(handleBlur).toBeCalled()
  300. })
  301. test('event:change', async () => {
  302. const content = ref('a')
  303. const value = ref('')
  304. const handleChange = (val: string) => {
  305. value.value = val
  306. }
  307. // NOTE: should be same as native's change behavior
  308. const wrapper = mount(() => (
  309. <Input
  310. placeholder="请输入内容"
  311. modelValue={content.value}
  312. onChange={handleChange}
  313. />
  314. ))
  315. const el = wrapper.find('input').element
  316. wrapper.vm
  317. const simulateEvent = (text: string, event: string) => {
  318. el.value = text
  319. el.dispatchEvent(new Event(event))
  320. }
  321. // simplified test, component should emit change when native does
  322. simulateEvent('2', 'change')
  323. await nextTick()
  324. expect(value.value).toBe('2')
  325. simulateEvent('1', 'input')
  326. await nextTick()
  327. expect(value.value).toBe('2')
  328. })
  329. test('event:clear', async () => {
  330. const handleClear = vi.fn()
  331. const handleInput = vi.fn()
  332. const content = ref('a')
  333. const wrapper = mount(() => (
  334. <Input
  335. placeholder="请输入内容"
  336. clearable
  337. v-model={content.value}
  338. onClear={handleClear}
  339. onInput={handleInput}
  340. />
  341. ))
  342. const input = wrapper.find('input')
  343. const vm = wrapper.vm
  344. // focus to show clear button
  345. await input.trigger('focus')
  346. await nextTick()
  347. vm.$el.querySelector('.el-input__clear').click()
  348. await nextTick()
  349. expect(content.value).toEqual('')
  350. expect(handleClear).toBeCalled()
  351. expect(handleInput).toBeCalled()
  352. })
  353. test('event:input', async () => {
  354. const handleInput = vi.fn()
  355. const content = ref('a')
  356. const wrapper = mount(() => (
  357. <Input
  358. placeholder="请输入内容"
  359. clearable
  360. modelValue={content.value}
  361. onInput={handleInput}
  362. />
  363. ))
  364. const inputWrapper = wrapper.find('input')
  365. const nativeInput = inputWrapper.element
  366. nativeInput.value = '1'
  367. await inputWrapper.trigger('compositionstart')
  368. await inputWrapper.trigger('input')
  369. nativeInput.value = '2'
  370. await inputWrapper.trigger('compositionupdate')
  371. await inputWrapper.trigger('input')
  372. await inputWrapper.trigger('compositionend')
  373. expect(handleInput).toBeCalledTimes(1)
  374. // native input value is controlled
  375. expect(content.value).toEqual('a')
  376. expect(nativeInput.value).toEqual('a')
  377. })
  378. })
  379. test('non-emit event such as keyup should work', async () => {
  380. const handleKeyup = vi.fn()
  381. const wrapper = mount(Input, {
  382. attrs: {
  383. onKeyup: handleKeyup,
  384. },
  385. })
  386. await wrapper.find('input').trigger('keyup')
  387. expect(handleKeyup).toBeCalledTimes(1)
  388. })
  389. test('input-style', async () => {
  390. const wrapper = mount(() => (
  391. <>
  392. <Input placeholder="请输入内容" input-style={{ color: 'red' }} />
  393. <Input
  394. placeholder="请输入内容"
  395. input-style={{ color: 'red' }}
  396. type="textarea"
  397. />
  398. </>
  399. ))
  400. const input = wrapper.find('input')
  401. const textarea = wrapper.find('textarea')
  402. await nextTick()
  403. expect(input.element.style.color === 'red').toBeTruthy()
  404. expect(textarea.element.style.color === 'red').toBeTruthy()
  405. })
  406. describe('Textarea Events', () => {
  407. test('event:keydown', async () => {
  408. const handleKeydown = vi.fn()
  409. const content = ref('')
  410. const wrapper = mount(() => (
  411. <Input
  412. type="textarea"
  413. modelValue={content.value}
  414. onKeydown={handleKeydown}
  415. />
  416. ))
  417. await wrapper.find('textarea').trigger('keydown')
  418. expect(handleKeydown).toBeCalledTimes(1)
  419. })
  420. })
  421. test('show-password icon', async () => {
  422. const password = ref('123456')
  423. const wrapper = mount(() => (
  424. <Input type="password" modelValue={password.value} show-password />
  425. ))
  426. const icon = wrapper.find('.el-input__icon.el-input__password')
  427. const d = icon.find('path').element.getAttribute('d')
  428. await icon.trigger('click')
  429. const d0 = icon.find('path').element.getAttribute('d')
  430. expect(d !== d0).toBeTruthy()
  431. })
  432. describe('form item accessibility integration', () => {
  433. test('automatic id attachment', async () => {
  434. const wrapper = mount(() => (
  435. <FormItem label="Foobar" data-test-ref="item">
  436. <Input data-test-ref="input" />
  437. </FormItem>
  438. ))
  439. await nextTick()
  440. const formItem = wrapper.find('[data-test-ref="item"]')
  441. const input = wrapper.find('[data-test-ref="input"]')
  442. const formItemLabel = formItem.find('.el-form-item__label')
  443. expect(formItem.attributes().role).toBeFalsy()
  444. expect(formItemLabel.attributes().for).toBe(input.attributes().id)
  445. })
  446. test('specified id attachment', async () => {
  447. const wrapper = mount(() => (
  448. <FormItem label="Foobar" data-test-ref="item">
  449. <Input id="foobar" data-test-ref="input" />
  450. </FormItem>
  451. ))
  452. await nextTick()
  453. const formItem = wrapper.find('[data-test-ref="item"]')
  454. const input = wrapper.find('[data-test-ref="input"]')
  455. const formItemLabel = formItem.find('.el-form-item__label')
  456. expect(formItem.attributes().role).toBeFalsy()
  457. expect(input.attributes().id).toBe('foobar')
  458. expect(formItemLabel.attributes().for).toBe(input.attributes().id)
  459. })
  460. test('form item role is group when multiple inputs', async () => {
  461. const wrapper = mount(() => (
  462. <FormItem label="Foobar" data-test-ref="item">
  463. <Input data-test-ref="input1" />
  464. <Input data-test-ref="input2" />
  465. </FormItem>
  466. ))
  467. await nextTick()
  468. const formItem = wrapper.find('[data-test-ref="item"]')
  469. expect(formItem.attributes().role).toBe('group')
  470. })
  471. })
  472. // TODO: validateEvent & input containes select cases should be added after the rest components finished
  473. // ...
  474. })