vModel.spec.ts 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448
  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 {
  227. number: null,
  228. trim: null,
  229. lazy: null,
  230. trimNumber: null,
  231. trimLazy: null,
  232. }
  233. },
  234. render() {
  235. return [
  236. withVModel(
  237. h('input', {
  238. class: 'number',
  239. 'onUpdate:modelValue': (val: any) => {
  240. this.number = val
  241. },
  242. }),
  243. this.number,
  244. {
  245. number: true,
  246. },
  247. ),
  248. withVModel(
  249. h('input', {
  250. class: 'trim',
  251. 'onUpdate:modelValue': (val: any) => {
  252. this.trim = val
  253. },
  254. }),
  255. this.trim,
  256. {
  257. trim: true,
  258. },
  259. ),
  260. withVModel(
  261. h('input', {
  262. class: 'trim-lazy',
  263. 'onUpdate:modelValue': (val: any) => {
  264. this.trimLazy = val
  265. },
  266. }),
  267. this.trim,
  268. {
  269. trim: true,
  270. lazy: true,
  271. },
  272. ),
  273. withVModel(
  274. h('input', {
  275. class: 'trim-number',
  276. 'onUpdate:modelValue': (val: any) => {
  277. this.trimNumber = val
  278. },
  279. }),
  280. this.trimNumber,
  281. {
  282. trim: true,
  283. number: true,
  284. },
  285. ),
  286. withVModel(
  287. h('input', {
  288. class: 'lazy',
  289. 'onUpdate:modelValue': (val: any) => {
  290. this.lazy = val
  291. },
  292. }),
  293. this.lazy,
  294. {
  295. lazy: true,
  296. },
  297. ),
  298. ]
  299. },
  300. })
  301. render(h(component), root)
  302. const number = root.querySelector('.number')
  303. const trim = root.querySelector('.trim')
  304. const trimNumber = root.querySelector('.trim-number')
  305. const trimLazy = root.querySelector('.trim-lazy')
  306. const lazy = root.querySelector('.lazy')
  307. const data = root._vnode.component.data
  308. number.value = '+01.2'
  309. triggerEvent('input', number)
  310. await nextTick()
  311. expect(data.number).toEqual(1.2)
  312. trim.value = ' hello, world '
  313. triggerEvent('input', trim)
  314. await nextTick()
  315. expect(data.trim).toEqual('hello, world')
  316. trimNumber.value = ' 1 '
  317. triggerEvent('input', trimNumber)
  318. await nextTick()
  319. expect(data.trimNumber).toEqual(1)
  320. trimNumber.value = ' +01.2 '
  321. triggerEvent('input', trimNumber)
  322. await nextTick()
  323. expect(data.trimNumber).toEqual(1.2)
  324. trimLazy.value = ' ddd '
  325. triggerEvent('change', trimLazy)
  326. await nextTick()
  327. expect(data.trimLazy).toEqual('ddd')
  328. lazy.value = 'foo'
  329. triggerEvent('change', lazy)
  330. await nextTick()
  331. expect(data.lazy).toEqual('foo')
  332. })
  333. it('should work with range', async () => {
  334. const component = defineComponent({
  335. data() {
  336. return { value: 25 }
  337. },
  338. render() {
  339. return [
  340. withVModel(
  341. h('input', {
  342. type: 'range',
  343. min: 1,
  344. max: 100,
  345. class: 'foo',
  346. 'onUpdate:modelValue': setValue.bind(this),
  347. }),
  348. this.value,
  349. {
  350. number: true,
  351. },
  352. ),
  353. withVModel(
  354. h('input', {
  355. type: 'range',
  356. min: 1,
  357. max: 100,
  358. class: 'bar',
  359. 'onUpdate:modelValue': setValue.bind(this),
  360. }),
  361. this.value,
  362. {
  363. lazy: true,
  364. },
  365. ),
  366. ]
  367. },
  368. })
  369. render(h(component), root)
  370. const foo = root.querySelector('.foo')
  371. const bar = root.querySelector('.bar')
  372. const data = root._vnode.component.data
  373. foo.value = 20
  374. triggerEvent('input', foo)
  375. await nextTick()
  376. expect(data.value).toEqual(20)
  377. foo.value = 200
  378. triggerEvent('input', foo)
  379. await nextTick()
  380. expect(data.value).toEqual(100)
  381. foo.value = -1
  382. triggerEvent('input', foo)
  383. await nextTick()
  384. expect(data.value).toEqual(1)
  385. bar.value = 30
  386. triggerEvent('change', bar)
  387. await nextTick()
  388. expect(data.value).toEqual('30')
  389. bar.value = 200
  390. triggerEvent('change', bar)
  391. await nextTick()
  392. expect(data.value).toEqual('100')
  393. bar.value = -1
  394. triggerEvent('change', bar)
  395. await nextTick()
  396. expect(data.value).toEqual('1')
  397. data.value = 60
  398. await nextTick()
  399. expect(foo.value).toEqual('60')
  400. expect(bar.value).toEqual('60')
  401. data.value = -1
  402. await nextTick()
  403. expect(foo.value).toEqual('1')
  404. expect(bar.value).toEqual('1')
  405. data.value = 200
  406. await nextTick()
  407. expect(foo.value).toEqual('100')
  408. expect(bar.value).toEqual('100')
  409. })
  410. it('should work with checkbox', async () => {
  411. const component = defineComponent({
  412. data() {
  413. return { value: null }
  414. },
  415. render() {
  416. return [
  417. withVModel(
  418. h('input', {
  419. type: 'checkbox',
  420. 'onUpdate:modelValue': setValue.bind(this),
  421. }),
  422. this.value,
  423. ),
  424. ]
  425. },
  426. })
  427. render(h(component), root)
  428. const input = root.querySelector('input')
  429. const data = root._vnode.component.data
  430. input.checked = true
  431. triggerEvent('change', input)
  432. await nextTick()
  433. expect(data.value).toEqual(true)
  434. data.value = false
  435. await nextTick()
  436. expect(input.checked).toEqual(false)
  437. data.value = true
  438. await nextTick()
  439. expect(input.checked).toEqual(true)
  440. input.checked = false
  441. triggerEvent('change', input)
  442. await nextTick()
  443. expect(data.value).toEqual(false)
  444. })
  445. it('should work with checkbox and true-value/false-value', async () => {
  446. const component = defineComponent({
  447. data() {
  448. return { value: 'yes' }
  449. },
  450. render() {
  451. return [
  452. withVModel(
  453. h('input', {
  454. type: 'checkbox',
  455. 'true-value': 'yes',
  456. 'false-value': 'no',
  457. 'onUpdate:modelValue': setValue.bind(this),
  458. }),
  459. this.value,
  460. ),
  461. ]
  462. },
  463. })
  464. render(h(component), root)
  465. const input = root.querySelector('input')
  466. const data = root._vnode.component.data
  467. // DOM checked state should respect initial true-value/false-value
  468. expect(input.checked).toEqual(true)
  469. input.checked = false
  470. triggerEvent('change', input)
  471. await nextTick()
  472. expect(data.value).toEqual('no')
  473. data.value = 'yes'
  474. await nextTick()
  475. expect(input.checked).toEqual(true)
  476. data.value = 'no'
  477. await nextTick()
  478. expect(input.checked).toEqual(false)
  479. input.checked = true
  480. triggerEvent('change', input)
  481. await nextTick()
  482. expect(data.value).toEqual('yes')
  483. })
  484. it('should work with checkbox and true-value/false-value with object values', async () => {
  485. const component = defineComponent({
  486. data() {
  487. return { value: null }
  488. },
  489. render() {
  490. return [
  491. withVModel(
  492. h('input', {
  493. type: 'checkbox',
  494. 'true-value': { yes: 'yes' },
  495. 'false-value': { no: 'no' },
  496. 'onUpdate:modelValue': setValue.bind(this),
  497. }),
  498. this.value,
  499. ),
  500. ]
  501. },
  502. })
  503. render(h(component), root)
  504. const input = root.querySelector('input')
  505. const data = root._vnode.component.data
  506. input.checked = true
  507. triggerEvent('change', input)
  508. await nextTick()
  509. expect(data.value).toEqual({ yes: 'yes' })
  510. data.value = { no: 'no' }
  511. await nextTick()
  512. expect(input.checked).toEqual(false)
  513. data.value = { yes: 'yes' }
  514. await nextTick()
  515. expect(input.checked).toEqual(true)
  516. input.checked = false
  517. triggerEvent('change', input)
  518. await nextTick()
  519. expect(data.value).toEqual({ no: 'no' })
  520. })
  521. it(`should support array as a checkbox model`, async () => {
  522. const component = defineComponent({
  523. data() {
  524. return { value: [] }
  525. },
  526. render() {
  527. return [
  528. withVModel(
  529. h('input', {
  530. type: 'checkbox',
  531. class: 'foo',
  532. value: 'foo',
  533. 'onUpdate:modelValue': setValue.bind(this),
  534. }),
  535. this.value,
  536. ),
  537. withVModel(
  538. h('input', {
  539. type: 'checkbox',
  540. class: 'bar',
  541. value: 'bar',
  542. 'onUpdate:modelValue': setValue.bind(this),
  543. }),
  544. this.value,
  545. ),
  546. ]
  547. },
  548. })
  549. render(h(component), root)
  550. const foo = root.querySelector('.foo')
  551. const bar = root.querySelector('.bar')
  552. const data = root._vnode.component.data
  553. foo.checked = true
  554. triggerEvent('change', foo)
  555. await nextTick()
  556. expect(data.value).toMatchObject(['foo'])
  557. bar.checked = true
  558. triggerEvent('change', bar)
  559. await nextTick()
  560. expect(data.value).toMatchObject(['foo', 'bar'])
  561. bar.checked = false
  562. triggerEvent('change', bar)
  563. await nextTick()
  564. expect(data.value).toMatchObject(['foo'])
  565. foo.checked = false
  566. triggerEvent('change', foo)
  567. await nextTick()
  568. expect(data.value).toMatchObject([])
  569. data.value = ['foo']
  570. await nextTick()
  571. expect(bar.checked).toEqual(false)
  572. expect(foo.checked).toEqual(true)
  573. data.value = ['bar']
  574. await nextTick()
  575. expect(foo.checked).toEqual(false)
  576. expect(bar.checked).toEqual(true)
  577. data.value = []
  578. await nextTick()
  579. expect(foo.checked).toEqual(false)
  580. expect(bar.checked).toEqual(false)
  581. })
  582. it(`should support Set as a checkbox model`, async () => {
  583. const component = defineComponent({
  584. data() {
  585. return { value: new Set() }
  586. },
  587. render() {
  588. return [
  589. withVModel(
  590. h('input', {
  591. type: 'checkbox',
  592. class: 'foo',
  593. value: 'foo',
  594. 'onUpdate:modelValue': setValue.bind(this),
  595. }),
  596. this.value,
  597. ),
  598. withVModel(
  599. h('input', {
  600. type: 'checkbox',
  601. class: 'bar',
  602. value: 'bar',
  603. 'onUpdate:modelValue': setValue.bind(this),
  604. }),
  605. this.value,
  606. ),
  607. ]
  608. },
  609. })
  610. render(h(component), root)
  611. const foo = root.querySelector('.foo')
  612. const bar = root.querySelector('.bar')
  613. const data = root._vnode.component.data
  614. foo.checked = true
  615. triggerEvent('change', foo)
  616. await nextTick()
  617. expect(data.value).toMatchObject(new Set(['foo']))
  618. bar.checked = true
  619. triggerEvent('change', bar)
  620. await nextTick()
  621. expect(data.value).toMatchObject(new Set(['foo', 'bar']))
  622. bar.checked = false
  623. triggerEvent('change', bar)
  624. await nextTick()
  625. expect(data.value).toMatchObject(new Set(['foo']))
  626. foo.checked = false
  627. triggerEvent('change', foo)
  628. await nextTick()
  629. expect(data.value).toMatchObject(new Set())
  630. data.value = new Set(['foo'])
  631. await nextTick()
  632. expect(bar.checked).toEqual(false)
  633. expect(foo.checked).toEqual(true)
  634. data.value = new Set(['bar'])
  635. await nextTick()
  636. expect(foo.checked).toEqual(false)
  637. expect(bar.checked).toEqual(true)
  638. data.value = new Set()
  639. await nextTick()
  640. expect(foo.checked).toEqual(false)
  641. expect(bar.checked).toEqual(false)
  642. })
  643. it('should not update DOM unnecessarily', async () => {
  644. const component = defineComponent({
  645. data() {
  646. return { value: true }
  647. },
  648. render() {
  649. return [
  650. withVModel(
  651. h('input', {
  652. type: 'checkbox',
  653. 'onUpdate:modelValue': setValue.bind(this),
  654. }),
  655. this.value,
  656. ),
  657. ]
  658. },
  659. })
  660. render(h(component), root)
  661. const input = root.querySelector('input')
  662. const data = root._vnode.component.data
  663. const setCheckedSpy = vi.spyOn(input, 'checked', 'set')
  664. // Trigger a change event without actually changing the value
  665. triggerEvent('change', input)
  666. await nextTick()
  667. expect(data.value).toEqual(true)
  668. expect(setCheckedSpy).not.toHaveBeenCalled()
  669. // Change the value and trigger a change event
  670. input.checked = false
  671. triggerEvent('change', input)
  672. await nextTick()
  673. expect(data.value).toEqual(false)
  674. expect(setCheckedSpy).toHaveBeenCalledTimes(1)
  675. setCheckedSpy.mockClear()
  676. data.value = false
  677. await nextTick()
  678. expect(input.checked).toEqual(false)
  679. expect(setCheckedSpy).not.toHaveBeenCalled()
  680. data.value = true
  681. await nextTick()
  682. expect(input.checked).toEqual(true)
  683. expect(setCheckedSpy).toHaveBeenCalledTimes(1)
  684. })
  685. it('should handle array values correctly without unnecessary updates', async () => {
  686. const component = defineComponent({
  687. data() {
  688. return { value: ['foo'] }
  689. },
  690. render() {
  691. return [
  692. withVModel(
  693. h('input', {
  694. type: 'checkbox',
  695. value: 'foo',
  696. 'onUpdate:modelValue': setValue.bind(this),
  697. }),
  698. this.value,
  699. ),
  700. withVModel(
  701. h('input', {
  702. type: 'checkbox',
  703. value: 'bar',
  704. 'onUpdate:modelValue': setValue.bind(this),
  705. }),
  706. this.value,
  707. ),
  708. ]
  709. },
  710. })
  711. render(h(component), root)
  712. const [foo, bar] = root.querySelectorAll('input')
  713. const data = root._vnode.component.data
  714. const setCheckedSpyFoo = vi.spyOn(foo, 'checked', 'set')
  715. const setCheckedSpyBar = vi.spyOn(bar, 'checked', 'set')
  716. expect(foo.checked).toEqual(true)
  717. expect(bar.checked).toEqual(false)
  718. triggerEvent('change', foo)
  719. await nextTick()
  720. expect(data.value).toEqual(['foo'])
  721. expect(setCheckedSpyFoo).not.toHaveBeenCalled()
  722. bar.checked = true
  723. triggerEvent('change', bar)
  724. await nextTick()
  725. expect(data.value).toEqual(['foo', 'bar'])
  726. expect(setCheckedSpyBar).toHaveBeenCalledTimes(1)
  727. setCheckedSpyFoo.mockClear()
  728. setCheckedSpyBar.mockClear()
  729. data.value = ['foo', 'bar']
  730. await nextTick()
  731. expect(setCheckedSpyFoo).not.toHaveBeenCalled()
  732. expect(setCheckedSpyBar).not.toHaveBeenCalled()
  733. data.value = ['bar']
  734. await nextTick()
  735. expect(setCheckedSpyFoo).toHaveBeenCalledTimes(1)
  736. expect(setCheckedSpyBar).not.toHaveBeenCalled()
  737. expect(foo.checked).toEqual(false)
  738. expect(bar.checked).toEqual(true)
  739. })
  740. it('should work with radio', async () => {
  741. const component = defineComponent({
  742. data() {
  743. return { value: null }
  744. },
  745. render() {
  746. return [
  747. withVModel(
  748. h('input', {
  749. type: 'radio',
  750. class: 'foo',
  751. value: 'foo',
  752. 'onUpdate:modelValue': setValue.bind(this),
  753. }),
  754. this.value,
  755. ),
  756. withVModel(
  757. h('input', {
  758. type: 'radio',
  759. class: 'bar',
  760. value: 'bar',
  761. 'onUpdate:modelValue': setValue.bind(this),
  762. }),
  763. this.value,
  764. ),
  765. ]
  766. },
  767. })
  768. render(h(component), root)
  769. const foo = root.querySelector('.foo')
  770. const bar = root.querySelector('.bar')
  771. const data = root._vnode.component.data
  772. foo.checked = true
  773. triggerEvent('change', foo)
  774. await nextTick()
  775. expect(data.value).toEqual('foo')
  776. bar.checked = true
  777. triggerEvent('change', bar)
  778. await nextTick()
  779. expect(data.value).toEqual('bar')
  780. data.value = null
  781. await nextTick()
  782. expect(foo.checked).toEqual(false)
  783. expect(bar.checked).toEqual(false)
  784. data.value = 'foo'
  785. await nextTick()
  786. expect(foo.checked).toEqual(true)
  787. expect(bar.checked).toEqual(false)
  788. data.value = 'bar'
  789. await nextTick()
  790. expect(foo.checked).toEqual(false)
  791. expect(bar.checked).toEqual(true)
  792. })
  793. it('should work with single select', async () => {
  794. const component = defineComponent({
  795. data() {
  796. return { value: null }
  797. },
  798. render() {
  799. return [
  800. withVModel(
  801. h(
  802. 'select',
  803. {
  804. value: null,
  805. 'onUpdate:modelValue': setValue.bind(this),
  806. },
  807. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  808. ),
  809. this.value,
  810. ),
  811. ]
  812. },
  813. })
  814. render(h(component), root)
  815. const input = root.querySelector('select')
  816. const foo = root.querySelector('option[value=foo]')
  817. const bar = root.querySelector('option[value=bar]')
  818. const data = root._vnode.component.data
  819. foo.selected = true
  820. triggerEvent('change', input)
  821. await nextTick()
  822. expect(data.value).toEqual('foo')
  823. foo.selected = false
  824. bar.selected = true
  825. triggerEvent('change', input)
  826. await nextTick()
  827. expect(data.value).toEqual('bar')
  828. foo.selected = false
  829. bar.selected = false
  830. data.value = 'foo'
  831. await nextTick()
  832. expect(input.value).toEqual('foo')
  833. expect(foo.selected).toEqual(true)
  834. expect(bar.selected).toEqual(false)
  835. foo.selected = true
  836. bar.selected = false
  837. data.value = 'bar'
  838. await nextTick()
  839. expect(input.value).toEqual('bar')
  840. expect(foo.selected).toEqual(false)
  841. expect(bar.selected).toEqual(true)
  842. })
  843. it('multiple select (model is Array)', async () => {
  844. const component = defineComponent({
  845. data() {
  846. return { value: [] }
  847. },
  848. render() {
  849. return [
  850. withVModel(
  851. h(
  852. 'select',
  853. {
  854. value: null,
  855. multiple: true,
  856. 'onUpdate:modelValue': setValue.bind(this),
  857. },
  858. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  859. ),
  860. this.value,
  861. ),
  862. ]
  863. },
  864. })
  865. render(h(component), root)
  866. const input = root.querySelector('select')
  867. const foo = root.querySelector('option[value=foo]')
  868. const bar = root.querySelector('option[value=bar]')
  869. const data = root._vnode.component.data
  870. foo.selected = true
  871. triggerEvent('change', input)
  872. await nextTick()
  873. expect(data.value).toMatchObject(['foo'])
  874. foo.selected = false
  875. bar.selected = true
  876. triggerEvent('change', input)
  877. await nextTick()
  878. expect(data.value).toMatchObject(['bar'])
  879. foo.selected = true
  880. bar.selected = true
  881. triggerEvent('change', input)
  882. await nextTick()
  883. expect(data.value).toMatchObject(['foo', 'bar'])
  884. foo.selected = false
  885. bar.selected = false
  886. data.value = ['foo']
  887. await nextTick()
  888. expect(input.value).toEqual('foo')
  889. expect(foo.selected).toEqual(true)
  890. expect(bar.selected).toEqual(false)
  891. foo.selected = false
  892. bar.selected = false
  893. data.value = ['foo', 'bar']
  894. await nextTick()
  895. expect(foo.selected).toEqual(true)
  896. expect(bar.selected).toEqual(true)
  897. })
  898. it('v-model.number should work with select tag', async () => {
  899. const component = defineComponent({
  900. data() {
  901. return { value: null }
  902. },
  903. render() {
  904. return [
  905. withVModel(
  906. h(
  907. 'select',
  908. {
  909. value: null,
  910. 'onUpdate:modelValue': setValue.bind(this),
  911. },
  912. [h('option', { value: '1' }), h('option', { value: '2' })],
  913. ),
  914. this.value,
  915. {
  916. number: true,
  917. },
  918. ),
  919. ]
  920. },
  921. })
  922. render(h(component), root)
  923. const input = root.querySelector('select')
  924. const one = root.querySelector('option[value="1"]')
  925. const data = root._vnode.component.data
  926. one.selected = true
  927. triggerEvent('change', input)
  928. await nextTick()
  929. expect(typeof data.value).toEqual('number')
  930. expect(data.value).toEqual(1)
  931. })
  932. it('v-model.number should work with multiple select', async () => {
  933. const component = defineComponent({
  934. data() {
  935. return { value: [] }
  936. },
  937. render() {
  938. return [
  939. withVModel(
  940. h(
  941. 'select',
  942. {
  943. value: null,
  944. multiple: true,
  945. 'onUpdate:modelValue': setValue.bind(this),
  946. },
  947. [h('option', { value: '1' }), h('option', { value: '2' })],
  948. ),
  949. this.value,
  950. {
  951. number: true,
  952. },
  953. ),
  954. ]
  955. },
  956. })
  957. render(h(component), root)
  958. const input = root.querySelector('select')
  959. const one = root.querySelector('option[value="1"]')
  960. const two = root.querySelector('option[value="2"]')
  961. const data = root._vnode.component.data
  962. one.selected = true
  963. two.selected = false
  964. triggerEvent('change', input)
  965. await nextTick()
  966. expect(data.value).toMatchObject([1])
  967. one.selected = false
  968. two.selected = true
  969. triggerEvent('change', input)
  970. await nextTick()
  971. expect(data.value).toMatchObject([2])
  972. one.selected = true
  973. two.selected = true
  974. triggerEvent('change', input)
  975. await nextTick()
  976. expect(data.value).toMatchObject([1, 2])
  977. one.selected = false
  978. two.selected = false
  979. data.value = [1]
  980. await nextTick()
  981. expect(one.selected).toEqual(true)
  982. expect(two.selected).toEqual(false)
  983. one.selected = false
  984. two.selected = false
  985. data.value = [1, 2]
  986. await nextTick()
  987. expect(one.selected).toEqual(true)
  988. expect(two.selected).toEqual(true)
  989. })
  990. it('multiple select (model is Array, option value is object)', async () => {
  991. const fooValue = { foo: 1 }
  992. const barValue = { bar: 1 }
  993. const component = defineComponent({
  994. data() {
  995. return { value: [] }
  996. },
  997. render() {
  998. return [
  999. withVModel(
  1000. h(
  1001. 'select',
  1002. {
  1003. value: null,
  1004. multiple: true,
  1005. 'onUpdate:modelValue': setValue.bind(this),
  1006. },
  1007. [
  1008. h('option', { value: fooValue }),
  1009. h('option', { value: barValue }),
  1010. ],
  1011. ),
  1012. this.value,
  1013. ),
  1014. ]
  1015. },
  1016. })
  1017. render(h(component), root)
  1018. await nextTick()
  1019. const input = root.querySelector('select')
  1020. const [foo, bar] = root.querySelectorAll('option')
  1021. const data = root._vnode.component.data
  1022. foo.selected = true
  1023. triggerEvent('change', input)
  1024. await nextTick()
  1025. expect(data.value).toMatchObject([fooValue])
  1026. foo.selected = false
  1027. bar.selected = true
  1028. triggerEvent('change', input)
  1029. await nextTick()
  1030. expect(data.value).toMatchObject([barValue])
  1031. foo.selected = true
  1032. bar.selected = true
  1033. triggerEvent('change', input)
  1034. await nextTick()
  1035. expect(data.value).toMatchObject([fooValue, barValue])
  1036. // reset
  1037. foo.selected = false
  1038. bar.selected = false
  1039. triggerEvent('change', input)
  1040. await nextTick()
  1041. expect(data.value).toMatchObject([])
  1042. data.value = [fooValue, barValue]
  1043. await nextTick()
  1044. expect(foo.selected).toEqual(true)
  1045. expect(bar.selected).toEqual(true)
  1046. // reset
  1047. foo.selected = false
  1048. bar.selected = false
  1049. triggerEvent('change', input)
  1050. await nextTick()
  1051. expect(data.value).toMatchObject([])
  1052. data.value = [{ foo: 1 }, { bar: 1 }]
  1053. await nextTick()
  1054. // looseEqual
  1055. expect(foo.selected).toEqual(true)
  1056. expect(bar.selected).toEqual(true)
  1057. })
  1058. it('multiple select (model is Set)', async () => {
  1059. const component = defineComponent({
  1060. data() {
  1061. return { value: new Set() }
  1062. },
  1063. render() {
  1064. return [
  1065. withVModel(
  1066. h(
  1067. 'select',
  1068. {
  1069. value: null,
  1070. multiple: true,
  1071. 'onUpdate:modelValue': setValue.bind(this),
  1072. },
  1073. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  1074. ),
  1075. this.value,
  1076. ),
  1077. ]
  1078. },
  1079. })
  1080. render(h(component), root)
  1081. const input = root.querySelector('select')
  1082. const foo = root.querySelector('option[value=foo]')
  1083. const bar = root.querySelector('option[value=bar]')
  1084. const data = root._vnode.component.data
  1085. foo.selected = true
  1086. triggerEvent('change', input)
  1087. await nextTick()
  1088. expect(data.value).toBeInstanceOf(Set)
  1089. expect(data.value).toMatchObject(new Set(['foo']))
  1090. foo.selected = false
  1091. bar.selected = true
  1092. triggerEvent('change', input)
  1093. await nextTick()
  1094. expect(data.value).toBeInstanceOf(Set)
  1095. expect(data.value).toMatchObject(new Set(['bar']))
  1096. foo.selected = true
  1097. bar.selected = true
  1098. triggerEvent('change', input)
  1099. await nextTick()
  1100. expect(data.value).toBeInstanceOf(Set)
  1101. expect(data.value).toMatchObject(new Set(['foo', 'bar']))
  1102. foo.selected = false
  1103. bar.selected = false
  1104. data.value = new Set(['foo'])
  1105. await nextTick()
  1106. expect(input.value).toEqual('foo')
  1107. expect(foo.selected).toEqual(true)
  1108. expect(bar.selected).toEqual(false)
  1109. foo.selected = false
  1110. bar.selected = false
  1111. data.value = new Set(['foo', 'bar'])
  1112. await nextTick()
  1113. expect(foo.selected).toEqual(true)
  1114. expect(bar.selected).toEqual(true)
  1115. })
  1116. it('multiple select (model is Set, option value is object)', async () => {
  1117. const fooValue = { foo: 1 }
  1118. const barValue = { bar: 1 }
  1119. const component = defineComponent({
  1120. data() {
  1121. return { value: new Set() }
  1122. },
  1123. render() {
  1124. return [
  1125. withVModel(
  1126. h(
  1127. 'select',
  1128. {
  1129. value: null,
  1130. multiple: true,
  1131. 'onUpdate:modelValue': setValue.bind(this),
  1132. },
  1133. [
  1134. h('option', { value: fooValue }),
  1135. h('option', { value: barValue }),
  1136. ],
  1137. ),
  1138. this.value,
  1139. ),
  1140. ]
  1141. },
  1142. })
  1143. render(h(component), root)
  1144. await nextTick()
  1145. const input = root.querySelector('select')
  1146. const [foo, bar] = root.querySelectorAll('option')
  1147. const data = root._vnode.component.data
  1148. foo.selected = true
  1149. triggerEvent('change', input)
  1150. await nextTick()
  1151. expect(data.value).toMatchObject(new Set([fooValue]))
  1152. foo.selected = false
  1153. bar.selected = true
  1154. triggerEvent('change', input)
  1155. await nextTick()
  1156. expect(data.value).toMatchObject(new Set([barValue]))
  1157. foo.selected = true
  1158. bar.selected = true
  1159. triggerEvent('change', input)
  1160. await nextTick()
  1161. expect(data.value).toMatchObject(new Set([fooValue, barValue]))
  1162. foo.selected = false
  1163. bar.selected = false
  1164. data.value = new Set([fooValue, barValue])
  1165. await nextTick()
  1166. expect(foo.selected).toEqual(true)
  1167. expect(bar.selected).toEqual(true)
  1168. foo.selected = false
  1169. bar.selected = false
  1170. data.value = new Set([{ foo: 1 }, { bar: 1 }])
  1171. await nextTick()
  1172. // without looseEqual, here is different from Array
  1173. expect(foo.selected).toEqual(false)
  1174. expect(bar.selected).toEqual(false)
  1175. })
  1176. it('should work with composition session', async () => {
  1177. const component = defineComponent({
  1178. data() {
  1179. return { value: '' }
  1180. },
  1181. render() {
  1182. return [
  1183. withVModel(
  1184. h('input', {
  1185. 'onUpdate:modelValue': setValue.bind(this),
  1186. }),
  1187. this.value,
  1188. ),
  1189. ]
  1190. },
  1191. })
  1192. render(h(component), root)
  1193. const input = root.querySelector('input')!
  1194. const data = root._vnode.component.data
  1195. expect(input.value).toEqual('')
  1196. //developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event
  1197. //compositionstart event could be fired after a user starts entering a Chinese character using a Pinyin IME
  1198. input.value = '使用拼音'
  1199. triggerEvent('compositionstart', input)
  1200. await nextTick()
  1201. expect(data.value).toEqual('')
  1202. // input event has no effect during composition session
  1203. input.value = '使用拼音输入'
  1204. triggerEvent('input', input)
  1205. await nextTick()
  1206. expect(data.value).toEqual('')
  1207. // After compositionend event being fired, an input event will be automatically trigger
  1208. triggerEvent('compositionend', input)
  1209. await nextTick()
  1210. expect(data.value).toEqual('使用拼音输入')
  1211. })
  1212. it('multiple select (model is number, option value is string)', async () => {
  1213. const component = defineComponent({
  1214. data() {
  1215. return {
  1216. value: [1, 2],
  1217. }
  1218. },
  1219. render() {
  1220. return [
  1221. withVModel(
  1222. h(
  1223. 'select',
  1224. {
  1225. multiple: true,
  1226. 'onUpdate:modelValue': setValue.bind(this),
  1227. },
  1228. [h('option', { value: '1' }), h('option', { value: '2' })],
  1229. ),
  1230. this.value,
  1231. ),
  1232. ]
  1233. },
  1234. })
  1235. render(h(component), root)
  1236. await nextTick()
  1237. const [foo, bar] = root.querySelectorAll('option')
  1238. expect(foo.selected).toEqual(true)
  1239. expect(bar.selected).toEqual(true)
  1240. })
  1241. // #10503
  1242. test('equal value with a leading 0 should trigger update.', async () => {
  1243. const setNum = function (this: any, value: any) {
  1244. this.num = value
  1245. }
  1246. const component = defineComponent({
  1247. data() {
  1248. return { num: 0 }
  1249. },
  1250. render() {
  1251. return [
  1252. withVModel(
  1253. h('input', {
  1254. id: 'input_num1',
  1255. type: 'number',
  1256. 'onUpdate:modelValue': setNum.bind(this),
  1257. }),
  1258. this.num,
  1259. ),
  1260. ]
  1261. },
  1262. })
  1263. render(h(component), root)
  1264. const data = root._vnode.component.data
  1265. const inputNum1 = root.querySelector('#input_num1')!
  1266. expect(inputNum1.value).toBe('0')
  1267. inputNum1.value = '01'
  1268. triggerEvent('input', inputNum1)
  1269. await nextTick()
  1270. expect(data.num).toBe(1)
  1271. expect(inputNum1.value).toBe('1')
  1272. })
  1273. })