vModel.spec.ts 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240
  1. import {
  2. type VNode,
  3. defineComponent,
  4. h,
  5. nextTick,
  6. ref,
  7. render,
  8. vModelDynamic,
  9. withDirectives,
  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 = vi.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 number input', async () => {
  63. const component = defineComponent({
  64. data() {
  65. return { value: null }
  66. },
  67. render() {
  68. return [
  69. withVModel(
  70. h('input', {
  71. type: 'number',
  72. 'onUpdate:modelValue': setValue.bind(this),
  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. expect(input.value).toEqual('')
  83. expect(input.type).toEqual('number')
  84. input.value = 1
  85. triggerEvent('input', input)
  86. await nextTick()
  87. expect(typeof data.value).toEqual('number')
  88. expect(data.value).toEqual(1)
  89. })
  90. // #7003
  91. it('should work with number input and be able to update rendering correctly', async () => {
  92. const setValue1 = function (this: any, value: any) {
  93. this.value1 = value
  94. }
  95. const setValue2 = function (this: any, value: any) {
  96. this.value2 = value
  97. }
  98. const component = defineComponent({
  99. data() {
  100. return { value1: 1.002, value2: 1.002 }
  101. },
  102. render() {
  103. return [
  104. withVModel(
  105. h('input', {
  106. id: 'input_num1',
  107. type: 'number',
  108. 'onUpdate:modelValue': setValue1.bind(this),
  109. }),
  110. this.value1,
  111. ),
  112. withVModel(
  113. h('input', {
  114. id: 'input_num2',
  115. type: 'number',
  116. 'onUpdate:modelValue': setValue2.bind(this),
  117. }),
  118. this.value2,
  119. ),
  120. ]
  121. },
  122. })
  123. render(h(component), root)
  124. const data = root._vnode.component.data
  125. const inputNum1 = root.querySelector('#input_num1')!
  126. expect(inputNum1.value).toBe('1.002')
  127. const inputNum2 = root.querySelector('#input_num2')!
  128. expect(inputNum2.value).toBe('1.002')
  129. inputNum1.value = '1.00'
  130. triggerEvent('input', inputNum1)
  131. await nextTick()
  132. expect(data.value1).toBe(1)
  133. inputNum2.value = '1.00'
  134. triggerEvent('input', inputNum2)
  135. await nextTick()
  136. expect(data.value2).toBe(1)
  137. expect(inputNum1.value).toBe('1.00')
  138. })
  139. it('should work with multiple listeners', async () => {
  140. const spy = vi.fn()
  141. const component = defineComponent({
  142. data() {
  143. return { value: null }
  144. },
  145. render() {
  146. return [
  147. withVModel(
  148. h('input', {
  149. 'onUpdate:modelValue': [setValue.bind(this), spy],
  150. }),
  151. this.value,
  152. ),
  153. ]
  154. },
  155. })
  156. render(h(component), root)
  157. const input = root.querySelector('input')!
  158. const data = root._vnode.component.data
  159. input.value = 'foo'
  160. triggerEvent('input', input)
  161. await nextTick()
  162. expect(data.value).toEqual('foo')
  163. expect(spy).toHaveBeenCalledWith('foo')
  164. })
  165. it('should work with updated listeners', async () => {
  166. const spy1 = vi.fn()
  167. const spy2 = vi.fn()
  168. const toggle = ref(true)
  169. const component = defineComponent({
  170. render() {
  171. return [
  172. withVModel(
  173. h('input', {
  174. 'onUpdate:modelValue': toggle.value ? spy1 : spy2,
  175. }),
  176. 'foo',
  177. ),
  178. ]
  179. },
  180. })
  181. render(h(component), root)
  182. const input = root.querySelector('input')!
  183. input.value = 'foo'
  184. triggerEvent('input', input)
  185. await nextTick()
  186. expect(spy1).toHaveBeenCalledWith('foo')
  187. // update listener
  188. toggle.value = false
  189. await nextTick()
  190. input.value = 'bar'
  191. triggerEvent('input', input)
  192. await nextTick()
  193. expect(spy1).not.toHaveBeenCalledWith('bar')
  194. expect(spy2).toHaveBeenCalledWith('bar')
  195. })
  196. it('should work with textarea', async () => {
  197. const component = defineComponent({
  198. data() {
  199. return { value: null }
  200. },
  201. render() {
  202. return [
  203. withVModel(
  204. h('textarea', {
  205. 'onUpdate:modelValue': setValue.bind(this),
  206. }),
  207. this.value,
  208. ),
  209. ]
  210. },
  211. })
  212. render(h(component), root)
  213. const input = root.querySelector('textarea')
  214. const data = root._vnode.component.data
  215. input.value = 'foo'
  216. triggerEvent('input', input)
  217. await nextTick()
  218. expect(data.value).toEqual('foo')
  219. data.value = 'bar'
  220. await nextTick()
  221. expect(input.value).toEqual('bar')
  222. })
  223. it('should support modifiers', async () => {
  224. const component = defineComponent({
  225. data() {
  226. return { number: null, trim: null, lazy: null, trimNumber: null }
  227. },
  228. render() {
  229. return [
  230. withVModel(
  231. h('input', {
  232. class: 'number',
  233. 'onUpdate:modelValue': (val: any) => {
  234. this.number = val
  235. },
  236. }),
  237. this.number,
  238. {
  239. number: true,
  240. },
  241. ),
  242. withVModel(
  243. h('input', {
  244. class: 'trim',
  245. 'onUpdate:modelValue': (val: any) => {
  246. this.trim = val
  247. },
  248. }),
  249. this.trim,
  250. {
  251. trim: true,
  252. },
  253. ),
  254. withVModel(
  255. h('input', {
  256. class: 'trim-number',
  257. 'onUpdate:modelValue': (val: any) => {
  258. this.trimNumber = val
  259. },
  260. }),
  261. this.trimNumber,
  262. {
  263. trim: true,
  264. number: true,
  265. },
  266. ),
  267. withVModel(
  268. h('input', {
  269. class: 'lazy',
  270. 'onUpdate:modelValue': (val: any) => {
  271. this.lazy = val
  272. },
  273. }),
  274. this.lazy,
  275. {
  276. lazy: true,
  277. },
  278. ),
  279. ]
  280. },
  281. })
  282. render(h(component), root)
  283. const number = root.querySelector('.number')
  284. const trim = root.querySelector('.trim')
  285. const trimNumber = root.querySelector('.trim-number')
  286. const lazy = root.querySelector('.lazy')
  287. const data = root._vnode.component.data
  288. number.value = '+01.2'
  289. triggerEvent('input', number)
  290. await nextTick()
  291. expect(data.number).toEqual(1.2)
  292. trim.value = ' hello, world '
  293. triggerEvent('input', trim)
  294. await nextTick()
  295. expect(data.trim).toEqual('hello, world')
  296. trimNumber.value = ' 1 '
  297. triggerEvent('input', trimNumber)
  298. await nextTick()
  299. expect(data.trimNumber).toEqual(1)
  300. trimNumber.value = ' +01.2 '
  301. triggerEvent('input', trimNumber)
  302. await nextTick()
  303. expect(data.trimNumber).toEqual(1.2)
  304. lazy.value = 'foo'
  305. triggerEvent('change', lazy)
  306. await nextTick()
  307. expect(data.lazy).toEqual('foo')
  308. })
  309. it('should work with range', async () => {
  310. const component = defineComponent({
  311. data() {
  312. return { value: 25 }
  313. },
  314. render() {
  315. return [
  316. withVModel(
  317. h('input', {
  318. type: 'range',
  319. min: 1,
  320. max: 100,
  321. class: 'foo',
  322. 'onUpdate:modelValue': setValue.bind(this),
  323. }),
  324. this.value,
  325. {
  326. number: true,
  327. },
  328. ),
  329. withVModel(
  330. h('input', {
  331. type: 'range',
  332. min: 1,
  333. max: 100,
  334. class: 'bar',
  335. 'onUpdate:modelValue': setValue.bind(this),
  336. }),
  337. this.value,
  338. {
  339. lazy: true,
  340. },
  341. ),
  342. ]
  343. },
  344. })
  345. render(h(component), root)
  346. const foo = root.querySelector('.foo')
  347. const bar = root.querySelector('.bar')
  348. const data = root._vnode.component.data
  349. foo.value = 20
  350. triggerEvent('input', foo)
  351. await nextTick()
  352. expect(data.value).toEqual(20)
  353. foo.value = 200
  354. triggerEvent('input', foo)
  355. await nextTick()
  356. expect(data.value).toEqual(100)
  357. foo.value = -1
  358. triggerEvent('input', foo)
  359. await nextTick()
  360. expect(data.value).toEqual(1)
  361. bar.value = 30
  362. triggerEvent('change', bar)
  363. await nextTick()
  364. expect(data.value).toEqual('30')
  365. bar.value = 200
  366. triggerEvent('change', bar)
  367. await nextTick()
  368. expect(data.value).toEqual('100')
  369. bar.value = -1
  370. triggerEvent('change', bar)
  371. await nextTick()
  372. expect(data.value).toEqual('1')
  373. data.value = 60
  374. await nextTick()
  375. expect(foo.value).toEqual('60')
  376. expect(bar.value).toEqual('60')
  377. data.value = -1
  378. await nextTick()
  379. expect(foo.value).toEqual('1')
  380. expect(bar.value).toEqual('1')
  381. data.value = 200
  382. await nextTick()
  383. expect(foo.value).toEqual('100')
  384. expect(bar.value).toEqual('100')
  385. })
  386. it('should work with checkbox', async () => {
  387. const component = defineComponent({
  388. data() {
  389. return { value: null }
  390. },
  391. render() {
  392. return [
  393. withVModel(
  394. h('input', {
  395. type: 'checkbox',
  396. 'onUpdate:modelValue': setValue.bind(this),
  397. }),
  398. this.value,
  399. ),
  400. ]
  401. },
  402. })
  403. render(h(component), root)
  404. const input = root.querySelector('input')
  405. const data = root._vnode.component.data
  406. input.checked = true
  407. triggerEvent('change', input)
  408. await nextTick()
  409. expect(data.value).toEqual(true)
  410. data.value = false
  411. await nextTick()
  412. expect(input.checked).toEqual(false)
  413. data.value = true
  414. await nextTick()
  415. expect(input.checked).toEqual(true)
  416. input.checked = false
  417. triggerEvent('change', input)
  418. await nextTick()
  419. expect(data.value).toEqual(false)
  420. })
  421. it('should work with checkbox and true-value/false-value', async () => {
  422. const component = defineComponent({
  423. data() {
  424. return { value: 'yes' }
  425. },
  426. render() {
  427. return [
  428. withVModel(
  429. h('input', {
  430. type: 'checkbox',
  431. 'true-value': 'yes',
  432. 'false-value': 'no',
  433. 'onUpdate:modelValue': setValue.bind(this),
  434. }),
  435. this.value,
  436. ),
  437. ]
  438. },
  439. })
  440. render(h(component), root)
  441. const input = root.querySelector('input')
  442. const data = root._vnode.component.data
  443. // DOM checked state should respect initial true-value/false-value
  444. expect(input.checked).toEqual(true)
  445. input.checked = false
  446. triggerEvent('change', input)
  447. await nextTick()
  448. expect(data.value).toEqual('no')
  449. data.value = 'yes'
  450. await nextTick()
  451. expect(input.checked).toEqual(true)
  452. data.value = 'no'
  453. await nextTick()
  454. expect(input.checked).toEqual(false)
  455. input.checked = true
  456. triggerEvent('change', input)
  457. await nextTick()
  458. expect(data.value).toEqual('yes')
  459. })
  460. it('should work with checkbox and true-value/false-value with object values', async () => {
  461. const component = defineComponent({
  462. data() {
  463. return { value: null }
  464. },
  465. render() {
  466. return [
  467. withVModel(
  468. h('input', {
  469. type: 'checkbox',
  470. 'true-value': { yes: 'yes' },
  471. 'false-value': { no: 'no' },
  472. 'onUpdate:modelValue': setValue.bind(this),
  473. }),
  474. this.value,
  475. ),
  476. ]
  477. },
  478. })
  479. render(h(component), root)
  480. const input = root.querySelector('input')
  481. const data = root._vnode.component.data
  482. input.checked = true
  483. triggerEvent('change', input)
  484. await nextTick()
  485. expect(data.value).toEqual({ yes: 'yes' })
  486. data.value = { no: 'no' }
  487. await nextTick()
  488. expect(input.checked).toEqual(false)
  489. data.value = { yes: 'yes' }
  490. await nextTick()
  491. expect(input.checked).toEqual(true)
  492. input.checked = false
  493. triggerEvent('change', input)
  494. await nextTick()
  495. expect(data.value).toEqual({ no: 'no' })
  496. })
  497. it(`should support array as a checkbox model`, async () => {
  498. const component = defineComponent({
  499. data() {
  500. return { value: [] }
  501. },
  502. render() {
  503. return [
  504. withVModel(
  505. h('input', {
  506. type: 'checkbox',
  507. class: 'foo',
  508. value: 'foo',
  509. 'onUpdate:modelValue': setValue.bind(this),
  510. }),
  511. this.value,
  512. ),
  513. withVModel(
  514. h('input', {
  515. type: 'checkbox',
  516. class: 'bar',
  517. value: 'bar',
  518. 'onUpdate:modelValue': setValue.bind(this),
  519. }),
  520. this.value,
  521. ),
  522. ]
  523. },
  524. })
  525. render(h(component), root)
  526. const foo = root.querySelector('.foo')
  527. const bar = root.querySelector('.bar')
  528. const data = root._vnode.component.data
  529. foo.checked = true
  530. triggerEvent('change', foo)
  531. await nextTick()
  532. expect(data.value).toMatchObject(['foo'])
  533. bar.checked = true
  534. triggerEvent('change', bar)
  535. await nextTick()
  536. expect(data.value).toMatchObject(['foo', 'bar'])
  537. bar.checked = false
  538. triggerEvent('change', bar)
  539. await nextTick()
  540. expect(data.value).toMatchObject(['foo'])
  541. foo.checked = false
  542. triggerEvent('change', foo)
  543. await nextTick()
  544. expect(data.value).toMatchObject([])
  545. data.value = ['foo']
  546. await nextTick()
  547. expect(bar.checked).toEqual(false)
  548. expect(foo.checked).toEqual(true)
  549. data.value = ['bar']
  550. await nextTick()
  551. expect(foo.checked).toEqual(false)
  552. expect(bar.checked).toEqual(true)
  553. data.value = []
  554. await nextTick()
  555. expect(foo.checked).toEqual(false)
  556. expect(bar.checked).toEqual(false)
  557. })
  558. it(`should support Set as a checkbox model`, async () => {
  559. const component = defineComponent({
  560. data() {
  561. return { value: new Set() }
  562. },
  563. render() {
  564. return [
  565. withVModel(
  566. h('input', {
  567. type: 'checkbox',
  568. class: 'foo',
  569. value: 'foo',
  570. 'onUpdate:modelValue': setValue.bind(this),
  571. }),
  572. this.value,
  573. ),
  574. withVModel(
  575. h('input', {
  576. type: 'checkbox',
  577. class: 'bar',
  578. value: 'bar',
  579. 'onUpdate:modelValue': setValue.bind(this),
  580. }),
  581. this.value,
  582. ),
  583. ]
  584. },
  585. })
  586. render(h(component), root)
  587. const foo = root.querySelector('.foo')
  588. const bar = root.querySelector('.bar')
  589. const data = root._vnode.component.data
  590. foo.checked = true
  591. triggerEvent('change', foo)
  592. await nextTick()
  593. expect(data.value).toMatchObject(new Set(['foo']))
  594. bar.checked = true
  595. triggerEvent('change', bar)
  596. await nextTick()
  597. expect(data.value).toMatchObject(new Set(['foo', 'bar']))
  598. bar.checked = false
  599. triggerEvent('change', bar)
  600. await nextTick()
  601. expect(data.value).toMatchObject(new Set(['foo']))
  602. foo.checked = false
  603. triggerEvent('change', foo)
  604. await nextTick()
  605. expect(data.value).toMatchObject(new Set())
  606. data.value = new Set(['foo'])
  607. await nextTick()
  608. expect(bar.checked).toEqual(false)
  609. expect(foo.checked).toEqual(true)
  610. data.value = new Set(['bar'])
  611. await nextTick()
  612. expect(foo.checked).toEqual(false)
  613. expect(bar.checked).toEqual(true)
  614. data.value = new Set()
  615. await nextTick()
  616. expect(foo.checked).toEqual(false)
  617. expect(bar.checked).toEqual(false)
  618. })
  619. it('should work with radio', async () => {
  620. const component = defineComponent({
  621. data() {
  622. return { value: null }
  623. },
  624. render() {
  625. return [
  626. withVModel(
  627. h('input', {
  628. type: 'radio',
  629. class: 'foo',
  630. value: 'foo',
  631. 'onUpdate:modelValue': setValue.bind(this),
  632. }),
  633. this.value,
  634. ),
  635. withVModel(
  636. h('input', {
  637. type: 'radio',
  638. class: 'bar',
  639. value: 'bar',
  640. 'onUpdate:modelValue': setValue.bind(this),
  641. }),
  642. this.value,
  643. ),
  644. ]
  645. },
  646. })
  647. render(h(component), root)
  648. const foo = root.querySelector('.foo')
  649. const bar = root.querySelector('.bar')
  650. const data = root._vnode.component.data
  651. foo.checked = true
  652. triggerEvent('change', foo)
  653. await nextTick()
  654. expect(data.value).toEqual('foo')
  655. bar.checked = true
  656. triggerEvent('change', bar)
  657. await nextTick()
  658. expect(data.value).toEqual('bar')
  659. data.value = null
  660. await nextTick()
  661. expect(foo.checked).toEqual(false)
  662. expect(bar.checked).toEqual(false)
  663. data.value = 'foo'
  664. await nextTick()
  665. expect(foo.checked).toEqual(true)
  666. expect(bar.checked).toEqual(false)
  667. data.value = 'bar'
  668. await nextTick()
  669. expect(foo.checked).toEqual(false)
  670. expect(bar.checked).toEqual(true)
  671. })
  672. it('should work with single select', async () => {
  673. const component = defineComponent({
  674. data() {
  675. return { value: null }
  676. },
  677. render() {
  678. return [
  679. withVModel(
  680. h(
  681. 'select',
  682. {
  683. value: null,
  684. 'onUpdate:modelValue': setValue.bind(this),
  685. },
  686. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  687. ),
  688. this.value,
  689. ),
  690. ]
  691. },
  692. })
  693. render(h(component), root)
  694. const input = root.querySelector('select')
  695. const foo = root.querySelector('option[value=foo]')
  696. const bar = root.querySelector('option[value=bar]')
  697. const data = root._vnode.component.data
  698. foo.selected = true
  699. triggerEvent('change', input)
  700. await nextTick()
  701. expect(data.value).toEqual('foo')
  702. foo.selected = false
  703. bar.selected = true
  704. triggerEvent('change', input)
  705. await nextTick()
  706. expect(data.value).toEqual('bar')
  707. foo.selected = false
  708. bar.selected = false
  709. data.value = 'foo'
  710. await nextTick()
  711. expect(input.value).toEqual('foo')
  712. expect(foo.selected).toEqual(true)
  713. expect(bar.selected).toEqual(false)
  714. foo.selected = true
  715. bar.selected = false
  716. data.value = 'bar'
  717. await nextTick()
  718. expect(input.value).toEqual('bar')
  719. expect(foo.selected).toEqual(false)
  720. expect(bar.selected).toEqual(true)
  721. })
  722. it('multiple select (model is Array)', async () => {
  723. const component = defineComponent({
  724. data() {
  725. return { value: [] }
  726. },
  727. render() {
  728. return [
  729. withVModel(
  730. h(
  731. 'select',
  732. {
  733. value: null,
  734. multiple: true,
  735. 'onUpdate:modelValue': setValue.bind(this),
  736. },
  737. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  738. ),
  739. this.value,
  740. ),
  741. ]
  742. },
  743. })
  744. render(h(component), root)
  745. const input = root.querySelector('select')
  746. const foo = root.querySelector('option[value=foo]')
  747. const bar = root.querySelector('option[value=bar]')
  748. const data = root._vnode.component.data
  749. foo.selected = true
  750. triggerEvent('change', input)
  751. await nextTick()
  752. expect(data.value).toMatchObject(['foo'])
  753. foo.selected = false
  754. bar.selected = true
  755. triggerEvent('change', input)
  756. await nextTick()
  757. expect(data.value).toMatchObject(['bar'])
  758. foo.selected = true
  759. bar.selected = true
  760. triggerEvent('change', input)
  761. await nextTick()
  762. expect(data.value).toMatchObject(['foo', 'bar'])
  763. foo.selected = false
  764. bar.selected = false
  765. data.value = ['foo']
  766. await nextTick()
  767. expect(input.value).toEqual('foo')
  768. expect(foo.selected).toEqual(true)
  769. expect(bar.selected).toEqual(false)
  770. foo.selected = false
  771. bar.selected = false
  772. data.value = ['foo', 'bar']
  773. await nextTick()
  774. expect(foo.selected).toEqual(true)
  775. expect(bar.selected).toEqual(true)
  776. })
  777. it('v-model.number should work with select tag', async () => {
  778. const component = defineComponent({
  779. data() {
  780. return { value: null }
  781. },
  782. render() {
  783. return [
  784. withVModel(
  785. h(
  786. 'select',
  787. {
  788. value: null,
  789. 'onUpdate:modelValue': setValue.bind(this),
  790. },
  791. [h('option', { value: '1' }), h('option', { value: '2' })],
  792. ),
  793. this.value,
  794. {
  795. number: true,
  796. },
  797. ),
  798. ]
  799. },
  800. })
  801. render(h(component), root)
  802. const input = root.querySelector('select')
  803. const one = root.querySelector('option[value="1"]')
  804. const data = root._vnode.component.data
  805. one.selected = true
  806. triggerEvent('change', input)
  807. await nextTick()
  808. expect(typeof data.value).toEqual('number')
  809. expect(data.value).toEqual(1)
  810. })
  811. it('v-model.number should work with multiple select', async () => {
  812. const component = defineComponent({
  813. data() {
  814. return { value: [] }
  815. },
  816. render() {
  817. return [
  818. withVModel(
  819. h(
  820. 'select',
  821. {
  822. value: null,
  823. multiple: true,
  824. 'onUpdate:modelValue': setValue.bind(this),
  825. },
  826. [h('option', { value: '1' }), h('option', { value: '2' })],
  827. ),
  828. this.value,
  829. {
  830. number: true,
  831. },
  832. ),
  833. ]
  834. },
  835. })
  836. render(h(component), root)
  837. const input = root.querySelector('select')
  838. const one = root.querySelector('option[value="1"]')
  839. const two = root.querySelector('option[value="2"]')
  840. const data = root._vnode.component.data
  841. one.selected = true
  842. two.selected = false
  843. triggerEvent('change', input)
  844. await nextTick()
  845. expect(data.value).toMatchObject([1])
  846. one.selected = false
  847. two.selected = true
  848. triggerEvent('change', input)
  849. await nextTick()
  850. expect(data.value).toMatchObject([2])
  851. one.selected = true
  852. two.selected = true
  853. triggerEvent('change', input)
  854. await nextTick()
  855. expect(data.value).toMatchObject([1, 2])
  856. one.selected = false
  857. two.selected = false
  858. data.value = [1]
  859. await nextTick()
  860. expect(one.selected).toEqual(true)
  861. expect(two.selected).toEqual(false)
  862. one.selected = false
  863. two.selected = false
  864. data.value = [1, 2]
  865. await nextTick()
  866. expect(one.selected).toEqual(true)
  867. expect(two.selected).toEqual(true)
  868. })
  869. it('multiple select (model is Array, option value is object)', async () => {
  870. const fooValue = { foo: 1 }
  871. const barValue = { bar: 1 }
  872. const component = defineComponent({
  873. data() {
  874. return { value: [] }
  875. },
  876. render() {
  877. return [
  878. withVModel(
  879. h(
  880. 'select',
  881. {
  882. value: null,
  883. multiple: true,
  884. 'onUpdate:modelValue': setValue.bind(this),
  885. },
  886. [
  887. h('option', { value: fooValue }),
  888. h('option', { value: barValue }),
  889. ],
  890. ),
  891. this.value,
  892. ),
  893. ]
  894. },
  895. })
  896. render(h(component), root)
  897. await nextTick()
  898. const input = root.querySelector('select')
  899. const [foo, bar] = root.querySelectorAll('option')
  900. const data = root._vnode.component.data
  901. foo.selected = true
  902. triggerEvent('change', input)
  903. await nextTick()
  904. expect(data.value).toMatchObject([fooValue])
  905. foo.selected = false
  906. bar.selected = true
  907. triggerEvent('change', input)
  908. await nextTick()
  909. expect(data.value).toMatchObject([barValue])
  910. foo.selected = true
  911. bar.selected = true
  912. triggerEvent('change', input)
  913. await nextTick()
  914. expect(data.value).toMatchObject([fooValue, barValue])
  915. // reset
  916. foo.selected = false
  917. bar.selected = false
  918. triggerEvent('change', input)
  919. await nextTick()
  920. expect(data.value).toMatchObject([])
  921. data.value = [fooValue, barValue]
  922. await nextTick()
  923. expect(foo.selected).toEqual(true)
  924. expect(bar.selected).toEqual(true)
  925. // reset
  926. foo.selected = false
  927. bar.selected = false
  928. triggerEvent('change', input)
  929. await nextTick()
  930. expect(data.value).toMatchObject([])
  931. data.value = [{ foo: 1 }, { bar: 1 }]
  932. await nextTick()
  933. // looseEqual
  934. expect(foo.selected).toEqual(true)
  935. expect(bar.selected).toEqual(true)
  936. })
  937. it('multiple select (model is Set)', async () => {
  938. const component = defineComponent({
  939. data() {
  940. return { value: new Set() }
  941. },
  942. render() {
  943. return [
  944. withVModel(
  945. h(
  946. 'select',
  947. {
  948. value: null,
  949. multiple: true,
  950. 'onUpdate:modelValue': setValue.bind(this),
  951. },
  952. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  953. ),
  954. this.value,
  955. ),
  956. ]
  957. },
  958. })
  959. render(h(component), root)
  960. const input = root.querySelector('select')
  961. const foo = root.querySelector('option[value=foo]')
  962. const bar = root.querySelector('option[value=bar]')
  963. const data = root._vnode.component.data
  964. foo.selected = true
  965. triggerEvent('change', input)
  966. await nextTick()
  967. expect(data.value).toBeInstanceOf(Set)
  968. expect(data.value).toMatchObject(new Set(['foo']))
  969. foo.selected = false
  970. bar.selected = true
  971. triggerEvent('change', input)
  972. await nextTick()
  973. expect(data.value).toBeInstanceOf(Set)
  974. expect(data.value).toMatchObject(new Set(['bar']))
  975. foo.selected = true
  976. bar.selected = true
  977. triggerEvent('change', input)
  978. await nextTick()
  979. expect(data.value).toBeInstanceOf(Set)
  980. expect(data.value).toMatchObject(new Set(['foo', 'bar']))
  981. foo.selected = false
  982. bar.selected = false
  983. data.value = new Set(['foo'])
  984. await nextTick()
  985. expect(input.value).toEqual('foo')
  986. expect(foo.selected).toEqual(true)
  987. expect(bar.selected).toEqual(false)
  988. foo.selected = false
  989. bar.selected = false
  990. data.value = new Set(['foo', 'bar'])
  991. await nextTick()
  992. expect(foo.selected).toEqual(true)
  993. expect(bar.selected).toEqual(true)
  994. })
  995. it('multiple select (model is Set, option value is object)', async () => {
  996. const fooValue = { foo: 1 }
  997. const barValue = { bar: 1 }
  998. const component = defineComponent({
  999. data() {
  1000. return { value: new Set() }
  1001. },
  1002. render() {
  1003. return [
  1004. withVModel(
  1005. h(
  1006. 'select',
  1007. {
  1008. value: null,
  1009. multiple: true,
  1010. 'onUpdate:modelValue': setValue.bind(this),
  1011. },
  1012. [
  1013. h('option', { value: fooValue }),
  1014. h('option', { value: barValue }),
  1015. ],
  1016. ),
  1017. this.value,
  1018. ),
  1019. ]
  1020. },
  1021. })
  1022. render(h(component), root)
  1023. await nextTick()
  1024. const input = root.querySelector('select')
  1025. const [foo, bar] = root.querySelectorAll('option')
  1026. const data = root._vnode.component.data
  1027. foo.selected = true
  1028. triggerEvent('change', input)
  1029. await nextTick()
  1030. expect(data.value).toMatchObject(new Set([fooValue]))
  1031. foo.selected = false
  1032. bar.selected = true
  1033. triggerEvent('change', input)
  1034. await nextTick()
  1035. expect(data.value).toMatchObject(new Set([barValue]))
  1036. foo.selected = true
  1037. bar.selected = true
  1038. triggerEvent('change', input)
  1039. await nextTick()
  1040. expect(data.value).toMatchObject(new Set([fooValue, barValue]))
  1041. foo.selected = false
  1042. bar.selected = false
  1043. data.value = new Set([fooValue, barValue])
  1044. await nextTick()
  1045. expect(foo.selected).toEqual(true)
  1046. expect(bar.selected).toEqual(true)
  1047. foo.selected = false
  1048. bar.selected = false
  1049. data.value = new Set([{ foo: 1 }, { bar: 1 }])
  1050. await nextTick()
  1051. // without looseEqual, here is different from Array
  1052. expect(foo.selected).toEqual(false)
  1053. expect(bar.selected).toEqual(false)
  1054. })
  1055. it('should work with composition session', async () => {
  1056. const component = defineComponent({
  1057. data() {
  1058. return { value: '' }
  1059. },
  1060. render() {
  1061. return [
  1062. withVModel(
  1063. h('input', {
  1064. 'onUpdate:modelValue': setValue.bind(this),
  1065. }),
  1066. this.value,
  1067. ),
  1068. ]
  1069. },
  1070. })
  1071. render(h(component), root)
  1072. const input = root.querySelector('input')!
  1073. const data = root._vnode.component.data
  1074. expect(input.value).toEqual('')
  1075. //developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event
  1076. //compositionstart event could be fired after a user starts entering a Chinese character using a Pinyin IME
  1077. input.value = '使用拼音'
  1078. triggerEvent('compositionstart', input)
  1079. await nextTick()
  1080. expect(data.value).toEqual('')
  1081. // input event has no effect during composition session
  1082. input.value = '使用拼音输入'
  1083. triggerEvent('input', input)
  1084. await nextTick()
  1085. expect(data.value).toEqual('')
  1086. // After compositionend event being fired, an input event will be automatically trigger
  1087. triggerEvent('compositionend', input)
  1088. await nextTick()
  1089. expect(data.value).toEqual('使用拼音输入')
  1090. })
  1091. })