vModel.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. import {
  2. h,
  3. render,
  4. nextTick,
  5. defineComponent,
  6. vModelDynamic,
  7. withDirectives,
  8. VNode,
  9. ref
  10. } from '@vue/runtime-dom'
  11. const triggerEvent = (type: string, el: Element) => {
  12. const event = new Event(type)
  13. el.dispatchEvent(event)
  14. }
  15. const withVModel = (node: VNode, arg: any, mods?: any) =>
  16. withDirectives(node, [[vModelDynamic, arg, '', mods]])
  17. const setValue = function(this: any, value: any) {
  18. this.value = value
  19. }
  20. let root: any
  21. beforeEach(() => {
  22. root = document.createElement('div') as any
  23. })
  24. describe('vModel', () => {
  25. it('should work with text input', async () => {
  26. const manualListener = jest.fn()
  27. const component = defineComponent({
  28. data() {
  29. return { value: null }
  30. },
  31. render() {
  32. return [
  33. withVModel(
  34. h('input', {
  35. 'onUpdate:modelValue': setValue.bind(this),
  36. onInput: () => {
  37. manualListener(data.value)
  38. }
  39. }),
  40. this.value
  41. )
  42. ]
  43. }
  44. })
  45. render(h(component), root)
  46. const input = root.querySelector('input')!
  47. const data = root._vnode.component.data
  48. expect(input.value).toEqual('')
  49. input.value = 'foo'
  50. triggerEvent('input', input)
  51. await nextTick()
  52. expect(data.value).toEqual('foo')
  53. // #1931
  54. expect(manualListener).toHaveBeenCalledWith('foo')
  55. data.value = 'bar'
  56. await nextTick()
  57. expect(input.value).toEqual('bar')
  58. data.value = undefined
  59. await nextTick()
  60. expect(input.value).toEqual('')
  61. })
  62. it('should work with multiple listeners', async () => {
  63. const spy = jest.fn()
  64. const component = defineComponent({
  65. data() {
  66. return { value: null }
  67. },
  68. render() {
  69. return [
  70. withVModel(
  71. h('input', {
  72. 'onUpdate:modelValue': [setValue.bind(this), spy]
  73. }),
  74. this.value
  75. )
  76. ]
  77. }
  78. })
  79. render(h(component), root)
  80. const input = root.querySelector('input')!
  81. const data = root._vnode.component.data
  82. input.value = 'foo'
  83. triggerEvent('input', input)
  84. await nextTick()
  85. expect(data.value).toEqual('foo')
  86. expect(spy).toHaveBeenCalledWith('foo')
  87. })
  88. it('should work with updated listeners', async () => {
  89. const spy1 = jest.fn()
  90. const spy2 = jest.fn()
  91. const toggle = ref(true)
  92. const component = defineComponent({
  93. render() {
  94. return [
  95. withVModel(
  96. h('input', {
  97. 'onUpdate:modelValue': toggle.value ? spy1 : spy2
  98. }),
  99. 'foo'
  100. )
  101. ]
  102. }
  103. })
  104. render(h(component), root)
  105. const input = root.querySelector('input')!
  106. input.value = 'foo'
  107. triggerEvent('input', input)
  108. await nextTick()
  109. expect(spy1).toHaveBeenCalledWith('foo')
  110. // update listener
  111. toggle.value = false
  112. await nextTick()
  113. input.value = 'bar'
  114. triggerEvent('input', input)
  115. await nextTick()
  116. expect(spy1).not.toHaveBeenCalledWith('bar')
  117. expect(spy2).toHaveBeenCalledWith('bar')
  118. })
  119. it('should work with textarea', async () => {
  120. const component = defineComponent({
  121. data() {
  122. return { value: null }
  123. },
  124. render() {
  125. return [
  126. withVModel(
  127. h('textarea', {
  128. 'onUpdate:modelValue': setValue.bind(this)
  129. }),
  130. this.value
  131. )
  132. ]
  133. }
  134. })
  135. render(h(component), root)
  136. const input = root.querySelector('textarea')
  137. const data = root._vnode.component.data
  138. input.value = 'foo'
  139. triggerEvent('input', input)
  140. await nextTick()
  141. expect(data.value).toEqual('foo')
  142. data.value = 'bar'
  143. await nextTick()
  144. expect(input.value).toEqual('bar')
  145. })
  146. it('should support modifiers', async () => {
  147. const component = defineComponent({
  148. data() {
  149. return { number: null, trim: null, lazy: null }
  150. },
  151. render() {
  152. return [
  153. withVModel(
  154. h('input', {
  155. class: 'number',
  156. 'onUpdate:modelValue': (val: any) => {
  157. this.number = val
  158. }
  159. }),
  160. this.number,
  161. {
  162. number: true
  163. }
  164. ),
  165. withVModel(
  166. h('input', {
  167. class: 'trim',
  168. 'onUpdate:modelValue': (val: any) => {
  169. this.trim = val
  170. }
  171. }),
  172. this.trim,
  173. {
  174. trim: true
  175. }
  176. ),
  177. withVModel(
  178. h('input', {
  179. class: 'lazy',
  180. 'onUpdate:modelValue': (val: any) => {
  181. this.lazy = val
  182. }
  183. }),
  184. this.lazy,
  185. {
  186. lazy: true
  187. }
  188. )
  189. ]
  190. }
  191. })
  192. render(h(component), root)
  193. const number = root.querySelector('.number')
  194. const trim = root.querySelector('.trim')
  195. const lazy = root.querySelector('.lazy')
  196. const data = root._vnode.component.data
  197. number.value = '+01.2'
  198. triggerEvent('input', number)
  199. await nextTick()
  200. expect(data.number).toEqual(1.2)
  201. trim.value = ' hello, world '
  202. triggerEvent('input', trim)
  203. await nextTick()
  204. expect(data.trim).toEqual('hello, world')
  205. lazy.value = 'foo'
  206. triggerEvent('change', lazy)
  207. await nextTick()
  208. expect(data.lazy).toEqual('foo')
  209. })
  210. it('should work with checkbox', async () => {
  211. const component = defineComponent({
  212. data() {
  213. return { value: null }
  214. },
  215. render() {
  216. return [
  217. withVModel(
  218. h('input', {
  219. type: 'checkbox',
  220. 'onUpdate:modelValue': setValue.bind(this)
  221. }),
  222. this.value
  223. )
  224. ]
  225. }
  226. })
  227. render(h(component), root)
  228. const input = root.querySelector('input')
  229. const data = root._vnode.component.data
  230. input.checked = true
  231. triggerEvent('change', input)
  232. await nextTick()
  233. expect(data.value).toEqual(true)
  234. data.value = false
  235. await nextTick()
  236. expect(input.checked).toEqual(false)
  237. data.value = true
  238. await nextTick()
  239. expect(input.checked).toEqual(true)
  240. input.checked = false
  241. triggerEvent('change', input)
  242. await nextTick()
  243. expect(data.value).toEqual(false)
  244. })
  245. it('should work with checkbox and true-value/false-value', async () => {
  246. const component = defineComponent({
  247. data() {
  248. return { value: null }
  249. },
  250. render() {
  251. return [
  252. withVModel(
  253. h('input', {
  254. type: 'checkbox',
  255. 'true-value': 'yes',
  256. 'false-value': 'no',
  257. 'onUpdate:modelValue': setValue.bind(this)
  258. }),
  259. this.value
  260. )
  261. ]
  262. }
  263. })
  264. render(h(component), root)
  265. const input = root.querySelector('input')
  266. const data = root._vnode.component.data
  267. input.checked = true
  268. triggerEvent('change', input)
  269. await nextTick()
  270. expect(data.value).toEqual('yes')
  271. data.value = 'no'
  272. await nextTick()
  273. expect(input.checked).toEqual(false)
  274. data.value = 'yes'
  275. await nextTick()
  276. expect(input.checked).toEqual(true)
  277. input.checked = false
  278. triggerEvent('change', input)
  279. await nextTick()
  280. expect(data.value).toEqual('no')
  281. })
  282. it('should work with checkbox and true-value/false-value with object values', async () => {
  283. const component = defineComponent({
  284. data() {
  285. return { value: null }
  286. },
  287. render() {
  288. return [
  289. withVModel(
  290. h('input', {
  291. type: 'checkbox',
  292. 'true-value': { yes: 'yes' },
  293. 'false-value': { no: 'no' },
  294. 'onUpdate:modelValue': setValue.bind(this)
  295. }),
  296. this.value
  297. )
  298. ]
  299. }
  300. })
  301. render(h(component), root)
  302. const input = root.querySelector('input')
  303. const data = root._vnode.component.data
  304. input.checked = true
  305. triggerEvent('change', input)
  306. await nextTick()
  307. expect(data.value).toEqual({ yes: 'yes' })
  308. data.value = { no: 'no' }
  309. await nextTick()
  310. expect(input.checked).toEqual(false)
  311. data.value = { yes: 'yes' }
  312. await nextTick()
  313. expect(input.checked).toEqual(true)
  314. input.checked = false
  315. triggerEvent('change', input)
  316. await nextTick()
  317. expect(data.value).toEqual({ no: 'no' })
  318. })
  319. it(`should support array as a checkbox model`, async () => {
  320. const component = defineComponent({
  321. data() {
  322. return { value: [] }
  323. },
  324. render() {
  325. return [
  326. withVModel(
  327. h('input', {
  328. type: 'checkbox',
  329. class: 'foo',
  330. value: 'foo',
  331. 'onUpdate:modelValue': setValue.bind(this)
  332. }),
  333. this.value
  334. ),
  335. withVModel(
  336. h('input', {
  337. type: 'checkbox',
  338. class: 'bar',
  339. value: 'bar',
  340. 'onUpdate:modelValue': setValue.bind(this)
  341. }),
  342. this.value
  343. )
  344. ]
  345. }
  346. })
  347. render(h(component), root)
  348. const foo = root.querySelector('.foo')
  349. const bar = root.querySelector('.bar')
  350. const data = root._vnode.component.data
  351. foo.checked = true
  352. triggerEvent('change', foo)
  353. await nextTick()
  354. expect(data.value).toMatchObject(['foo'])
  355. bar.checked = true
  356. triggerEvent('change', bar)
  357. await nextTick()
  358. expect(data.value).toMatchObject(['foo', 'bar'])
  359. bar.checked = false
  360. triggerEvent('change', bar)
  361. await nextTick()
  362. expect(data.value).toMatchObject(['foo'])
  363. foo.checked = false
  364. triggerEvent('change', foo)
  365. await nextTick()
  366. expect(data.value).toMatchObject([])
  367. data.value = ['foo']
  368. await nextTick()
  369. expect(bar.checked).toEqual(false)
  370. expect(foo.checked).toEqual(true)
  371. data.value = ['bar']
  372. await nextTick()
  373. expect(foo.checked).toEqual(false)
  374. expect(bar.checked).toEqual(true)
  375. data.value = []
  376. await nextTick()
  377. expect(foo.checked).toEqual(false)
  378. expect(bar.checked).toEqual(false)
  379. })
  380. it('should work with radio', async () => {
  381. const component = defineComponent({
  382. data() {
  383. return { value: null }
  384. },
  385. render() {
  386. return [
  387. withVModel(
  388. h('input', {
  389. type: 'radio',
  390. class: 'foo',
  391. value: 'foo',
  392. 'onUpdate:modelValue': setValue.bind(this)
  393. }),
  394. this.value
  395. ),
  396. withVModel(
  397. h('input', {
  398. type: 'radio',
  399. class: 'bar',
  400. value: 'bar',
  401. 'onUpdate:modelValue': setValue.bind(this)
  402. }),
  403. this.value
  404. )
  405. ]
  406. }
  407. })
  408. render(h(component), root)
  409. const foo = root.querySelector('.foo')
  410. const bar = root.querySelector('.bar')
  411. const data = root._vnode.component.data
  412. foo.checked = true
  413. triggerEvent('change', foo)
  414. await nextTick()
  415. expect(data.value).toEqual('foo')
  416. bar.checked = true
  417. triggerEvent('change', bar)
  418. await nextTick()
  419. expect(data.value).toEqual('bar')
  420. data.value = null
  421. await nextTick()
  422. expect(foo.checked).toEqual(false)
  423. expect(bar.checked).toEqual(false)
  424. data.value = 'foo'
  425. await nextTick()
  426. expect(foo.checked).toEqual(true)
  427. expect(bar.checked).toEqual(false)
  428. data.value = 'bar'
  429. await nextTick()
  430. expect(foo.checked).toEqual(false)
  431. expect(bar.checked).toEqual(true)
  432. })
  433. it('should work with single select', async () => {
  434. const component = defineComponent({
  435. data() {
  436. return { value: null }
  437. },
  438. render() {
  439. return [
  440. withVModel(
  441. h(
  442. 'select',
  443. {
  444. value: null,
  445. 'onUpdate:modelValue': setValue.bind(this)
  446. },
  447. [h('option', { value: 'foo' }), h('option', { value: 'bar' })]
  448. ),
  449. this.value
  450. )
  451. ]
  452. }
  453. })
  454. render(h(component), root)
  455. const input = root.querySelector('select')
  456. const foo = root.querySelector('option[value=foo]')
  457. const bar = root.querySelector('option[value=bar]')
  458. const data = root._vnode.component.data
  459. foo.selected = true
  460. triggerEvent('change', input)
  461. await nextTick()
  462. expect(data.value).toEqual('foo')
  463. foo.selected = false
  464. bar.selected = true
  465. triggerEvent('change', input)
  466. await nextTick()
  467. expect(data.value).toEqual('bar')
  468. foo.selected = false
  469. bar.selected = false
  470. data.value = 'foo'
  471. await nextTick()
  472. expect(input.value).toEqual('foo')
  473. expect(foo.selected).toEqual(true)
  474. expect(bar.selected).toEqual(false)
  475. foo.selected = true
  476. bar.selected = false
  477. data.value = 'bar'
  478. await nextTick()
  479. expect(input.value).toEqual('bar')
  480. expect(foo.selected).toEqual(false)
  481. expect(bar.selected).toEqual(true)
  482. })
  483. it('should work with multiple select', async () => {
  484. const component = defineComponent({
  485. data() {
  486. return { value: [] }
  487. },
  488. render() {
  489. return [
  490. withVModel(
  491. h(
  492. 'select',
  493. {
  494. value: null,
  495. multiple: true,
  496. 'onUpdate:modelValue': setValue.bind(this)
  497. },
  498. [h('option', { value: 'foo' }), h('option', { value: 'bar' })]
  499. ),
  500. this.value
  501. )
  502. ]
  503. }
  504. })
  505. render(h(component), root)
  506. const input = root.querySelector('select')
  507. const foo = root.querySelector('option[value=foo]')
  508. const bar = root.querySelector('option[value=bar]')
  509. const data = root._vnode.component.data
  510. foo.selected = true
  511. triggerEvent('change', input)
  512. await nextTick()
  513. expect(data.value).toMatchObject(['foo'])
  514. foo.selected = false
  515. bar.selected = true
  516. triggerEvent('change', input)
  517. await nextTick()
  518. expect(data.value).toMatchObject(['bar'])
  519. foo.selected = true
  520. bar.selected = true
  521. triggerEvent('change', input)
  522. await nextTick()
  523. expect(data.value).toMatchObject(['foo', 'bar'])
  524. foo.selected = false
  525. bar.selected = false
  526. data.value = ['foo']
  527. await nextTick()
  528. expect(input.value).toEqual('foo')
  529. expect(foo.selected).toEqual(true)
  530. expect(bar.selected).toEqual(false)
  531. foo.selected = false
  532. bar.selected = false
  533. data.value = ['foo', 'bar']
  534. await nextTick()
  535. expect(foo.selected).toEqual(true)
  536. expect(bar.selected).toEqual(true)
  537. })
  538. it('should work with composition session', async () => {
  539. const component = defineComponent({
  540. data() {
  541. return { value: '' }
  542. },
  543. render() {
  544. return [
  545. withVModel(
  546. h('input', {
  547. 'onUpdate:modelValue': setValue.bind(this)
  548. }),
  549. this.value
  550. )
  551. ]
  552. }
  553. })
  554. render(h(component), root)
  555. const input = root.querySelector('input')!
  556. const data = root._vnode.component.data
  557. expect(input.value).toEqual('')
  558. //developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event
  559. //compositionstart event could be fired after a user starts entering a Chinese character using a Pinyin IME
  560. input.value = '使用拼音'
  561. triggerEvent('compositionstart', input)
  562. await nextTick()
  563. expect(data.value).toEqual('')
  564. // input event has no effect during composition session
  565. input.value = '使用拼音输入'
  566. triggerEvent('input', input)
  567. await nextTick()
  568. expect(data.value).toEqual('')
  569. // After compositionend event being fired, an input event will be automatically trigger
  570. triggerEvent('compositionend', input)
  571. await nextTick()
  572. expect(data.value).toEqual('使用拼音输入')
  573. })
  574. })