vModel.spec.ts 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334
  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 work with radio', async () => {
  644. const component = defineComponent({
  645. data() {
  646. return { value: null }
  647. },
  648. render() {
  649. return [
  650. withVModel(
  651. h('input', {
  652. type: 'radio',
  653. class: 'foo',
  654. value: 'foo',
  655. 'onUpdate:modelValue': setValue.bind(this),
  656. }),
  657. this.value,
  658. ),
  659. withVModel(
  660. h('input', {
  661. type: 'radio',
  662. class: 'bar',
  663. value: 'bar',
  664. 'onUpdate:modelValue': setValue.bind(this),
  665. }),
  666. this.value,
  667. ),
  668. ]
  669. },
  670. })
  671. render(h(component), root)
  672. const foo = root.querySelector('.foo')
  673. const bar = root.querySelector('.bar')
  674. const data = root._vnode.component.data
  675. foo.checked = true
  676. triggerEvent('change', foo)
  677. await nextTick()
  678. expect(data.value).toEqual('foo')
  679. bar.checked = true
  680. triggerEvent('change', bar)
  681. await nextTick()
  682. expect(data.value).toEqual('bar')
  683. data.value = null
  684. await nextTick()
  685. expect(foo.checked).toEqual(false)
  686. expect(bar.checked).toEqual(false)
  687. data.value = 'foo'
  688. await nextTick()
  689. expect(foo.checked).toEqual(true)
  690. expect(bar.checked).toEqual(false)
  691. data.value = 'bar'
  692. await nextTick()
  693. expect(foo.checked).toEqual(false)
  694. expect(bar.checked).toEqual(true)
  695. })
  696. it('should work with single select', async () => {
  697. const component = defineComponent({
  698. data() {
  699. return { value: null }
  700. },
  701. render() {
  702. return [
  703. withVModel(
  704. h(
  705. 'select',
  706. {
  707. value: null,
  708. 'onUpdate:modelValue': setValue.bind(this),
  709. },
  710. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  711. ),
  712. this.value,
  713. ),
  714. ]
  715. },
  716. })
  717. render(h(component), root)
  718. const input = root.querySelector('select')
  719. const foo = root.querySelector('option[value=foo]')
  720. const bar = root.querySelector('option[value=bar]')
  721. const data = root._vnode.component.data
  722. foo.selected = true
  723. triggerEvent('change', input)
  724. await nextTick()
  725. expect(data.value).toEqual('foo')
  726. foo.selected = false
  727. bar.selected = true
  728. triggerEvent('change', input)
  729. await nextTick()
  730. expect(data.value).toEqual('bar')
  731. foo.selected = false
  732. bar.selected = false
  733. data.value = 'foo'
  734. await nextTick()
  735. expect(input.value).toEqual('foo')
  736. expect(foo.selected).toEqual(true)
  737. expect(bar.selected).toEqual(false)
  738. foo.selected = true
  739. bar.selected = false
  740. data.value = 'bar'
  741. await nextTick()
  742. expect(input.value).toEqual('bar')
  743. expect(foo.selected).toEqual(false)
  744. expect(bar.selected).toEqual(true)
  745. })
  746. it('multiple select (model is Array)', async () => {
  747. const component = defineComponent({
  748. data() {
  749. return { value: [] }
  750. },
  751. render() {
  752. return [
  753. withVModel(
  754. h(
  755. 'select',
  756. {
  757. value: null,
  758. multiple: true,
  759. 'onUpdate:modelValue': setValue.bind(this),
  760. },
  761. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  762. ),
  763. this.value,
  764. ),
  765. ]
  766. },
  767. })
  768. render(h(component), root)
  769. const input = root.querySelector('select')
  770. const foo = root.querySelector('option[value=foo]')
  771. const bar = root.querySelector('option[value=bar]')
  772. const data = root._vnode.component.data
  773. foo.selected = true
  774. triggerEvent('change', input)
  775. await nextTick()
  776. expect(data.value).toMatchObject(['foo'])
  777. foo.selected = false
  778. bar.selected = true
  779. triggerEvent('change', input)
  780. await nextTick()
  781. expect(data.value).toMatchObject(['bar'])
  782. foo.selected = true
  783. bar.selected = true
  784. triggerEvent('change', input)
  785. await nextTick()
  786. expect(data.value).toMatchObject(['foo', 'bar'])
  787. foo.selected = false
  788. bar.selected = false
  789. data.value = ['foo']
  790. await nextTick()
  791. expect(input.value).toEqual('foo')
  792. expect(foo.selected).toEqual(true)
  793. expect(bar.selected).toEqual(false)
  794. foo.selected = false
  795. bar.selected = false
  796. data.value = ['foo', 'bar']
  797. await nextTick()
  798. expect(foo.selected).toEqual(true)
  799. expect(bar.selected).toEqual(true)
  800. })
  801. it('v-model.number should work with select tag', async () => {
  802. const component = defineComponent({
  803. data() {
  804. return { value: null }
  805. },
  806. render() {
  807. return [
  808. withVModel(
  809. h(
  810. 'select',
  811. {
  812. value: null,
  813. 'onUpdate:modelValue': setValue.bind(this),
  814. },
  815. [h('option', { value: '1' }), h('option', { value: '2' })],
  816. ),
  817. this.value,
  818. {
  819. number: true,
  820. },
  821. ),
  822. ]
  823. },
  824. })
  825. render(h(component), root)
  826. const input = root.querySelector('select')
  827. const one = root.querySelector('option[value="1"]')
  828. const data = root._vnode.component.data
  829. one.selected = true
  830. triggerEvent('change', input)
  831. await nextTick()
  832. expect(typeof data.value).toEqual('number')
  833. expect(data.value).toEqual(1)
  834. })
  835. it('v-model.number should work with multiple select', async () => {
  836. const component = defineComponent({
  837. data() {
  838. return { value: [] }
  839. },
  840. render() {
  841. return [
  842. withVModel(
  843. h(
  844. 'select',
  845. {
  846. value: null,
  847. multiple: true,
  848. 'onUpdate:modelValue': setValue.bind(this),
  849. },
  850. [h('option', { value: '1' }), h('option', { value: '2' })],
  851. ),
  852. this.value,
  853. {
  854. number: true,
  855. },
  856. ),
  857. ]
  858. },
  859. })
  860. render(h(component), root)
  861. const input = root.querySelector('select')
  862. const one = root.querySelector('option[value="1"]')
  863. const two = root.querySelector('option[value="2"]')
  864. const data = root._vnode.component.data
  865. one.selected = true
  866. two.selected = false
  867. triggerEvent('change', input)
  868. await nextTick()
  869. expect(data.value).toMatchObject([1])
  870. one.selected = false
  871. two.selected = true
  872. triggerEvent('change', input)
  873. await nextTick()
  874. expect(data.value).toMatchObject([2])
  875. one.selected = true
  876. two.selected = true
  877. triggerEvent('change', input)
  878. await nextTick()
  879. expect(data.value).toMatchObject([1, 2])
  880. one.selected = false
  881. two.selected = false
  882. data.value = [1]
  883. await nextTick()
  884. expect(one.selected).toEqual(true)
  885. expect(two.selected).toEqual(false)
  886. one.selected = false
  887. two.selected = false
  888. data.value = [1, 2]
  889. await nextTick()
  890. expect(one.selected).toEqual(true)
  891. expect(two.selected).toEqual(true)
  892. })
  893. it('multiple select (model is Array, option value is object)', async () => {
  894. const fooValue = { foo: 1 }
  895. const barValue = { bar: 1 }
  896. const component = defineComponent({
  897. data() {
  898. return { value: [] }
  899. },
  900. render() {
  901. return [
  902. withVModel(
  903. h(
  904. 'select',
  905. {
  906. value: null,
  907. multiple: true,
  908. 'onUpdate:modelValue': setValue.bind(this),
  909. },
  910. [
  911. h('option', { value: fooValue }),
  912. h('option', { value: barValue }),
  913. ],
  914. ),
  915. this.value,
  916. ),
  917. ]
  918. },
  919. })
  920. render(h(component), root)
  921. await nextTick()
  922. const input = root.querySelector('select')
  923. const [foo, bar] = root.querySelectorAll('option')
  924. const data = root._vnode.component.data
  925. foo.selected = true
  926. triggerEvent('change', input)
  927. await nextTick()
  928. expect(data.value).toMatchObject([fooValue])
  929. foo.selected = false
  930. bar.selected = true
  931. triggerEvent('change', input)
  932. await nextTick()
  933. expect(data.value).toMatchObject([barValue])
  934. foo.selected = true
  935. bar.selected = true
  936. triggerEvent('change', input)
  937. await nextTick()
  938. expect(data.value).toMatchObject([fooValue, barValue])
  939. // reset
  940. foo.selected = false
  941. bar.selected = false
  942. triggerEvent('change', input)
  943. await nextTick()
  944. expect(data.value).toMatchObject([])
  945. data.value = [fooValue, barValue]
  946. await nextTick()
  947. expect(foo.selected).toEqual(true)
  948. expect(bar.selected).toEqual(true)
  949. // reset
  950. foo.selected = false
  951. bar.selected = false
  952. triggerEvent('change', input)
  953. await nextTick()
  954. expect(data.value).toMatchObject([])
  955. data.value = [{ foo: 1 }, { bar: 1 }]
  956. await nextTick()
  957. // looseEqual
  958. expect(foo.selected).toEqual(true)
  959. expect(bar.selected).toEqual(true)
  960. })
  961. it('multiple select (model is Set)', async () => {
  962. const component = defineComponent({
  963. data() {
  964. return { value: new Set() }
  965. },
  966. render() {
  967. return [
  968. withVModel(
  969. h(
  970. 'select',
  971. {
  972. value: null,
  973. multiple: true,
  974. 'onUpdate:modelValue': setValue.bind(this),
  975. },
  976. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  977. ),
  978. this.value,
  979. ),
  980. ]
  981. },
  982. })
  983. render(h(component), root)
  984. const input = root.querySelector('select')
  985. const foo = root.querySelector('option[value=foo]')
  986. const bar = root.querySelector('option[value=bar]')
  987. const data = root._vnode.component.data
  988. foo.selected = true
  989. triggerEvent('change', input)
  990. await nextTick()
  991. expect(data.value).toBeInstanceOf(Set)
  992. expect(data.value).toMatchObject(new Set(['foo']))
  993. foo.selected = false
  994. bar.selected = true
  995. triggerEvent('change', input)
  996. await nextTick()
  997. expect(data.value).toBeInstanceOf(Set)
  998. expect(data.value).toMatchObject(new Set(['bar']))
  999. foo.selected = true
  1000. bar.selected = true
  1001. triggerEvent('change', input)
  1002. await nextTick()
  1003. expect(data.value).toBeInstanceOf(Set)
  1004. expect(data.value).toMatchObject(new Set(['foo', 'bar']))
  1005. foo.selected = false
  1006. bar.selected = false
  1007. data.value = new Set(['foo'])
  1008. await nextTick()
  1009. expect(input.value).toEqual('foo')
  1010. expect(foo.selected).toEqual(true)
  1011. expect(bar.selected).toEqual(false)
  1012. foo.selected = false
  1013. bar.selected = false
  1014. data.value = new Set(['foo', 'bar'])
  1015. await nextTick()
  1016. expect(foo.selected).toEqual(true)
  1017. expect(bar.selected).toEqual(true)
  1018. })
  1019. it('multiple select (model is Set, option value is object)', async () => {
  1020. const fooValue = { foo: 1 }
  1021. const barValue = { bar: 1 }
  1022. const component = defineComponent({
  1023. data() {
  1024. return { value: new Set() }
  1025. },
  1026. render() {
  1027. return [
  1028. withVModel(
  1029. h(
  1030. 'select',
  1031. {
  1032. value: null,
  1033. multiple: true,
  1034. 'onUpdate:modelValue': setValue.bind(this),
  1035. },
  1036. [
  1037. h('option', { value: fooValue }),
  1038. h('option', { value: barValue }),
  1039. ],
  1040. ),
  1041. this.value,
  1042. ),
  1043. ]
  1044. },
  1045. })
  1046. render(h(component), root)
  1047. await nextTick()
  1048. const input = root.querySelector('select')
  1049. const [foo, bar] = root.querySelectorAll('option')
  1050. const data = root._vnode.component.data
  1051. foo.selected = true
  1052. triggerEvent('change', input)
  1053. await nextTick()
  1054. expect(data.value).toMatchObject(new Set([fooValue]))
  1055. foo.selected = false
  1056. bar.selected = true
  1057. triggerEvent('change', input)
  1058. await nextTick()
  1059. expect(data.value).toMatchObject(new Set([barValue]))
  1060. foo.selected = true
  1061. bar.selected = true
  1062. triggerEvent('change', input)
  1063. await nextTick()
  1064. expect(data.value).toMatchObject(new Set([fooValue, barValue]))
  1065. foo.selected = false
  1066. bar.selected = false
  1067. data.value = new Set([fooValue, barValue])
  1068. await nextTick()
  1069. expect(foo.selected).toEqual(true)
  1070. expect(bar.selected).toEqual(true)
  1071. foo.selected = false
  1072. bar.selected = false
  1073. data.value = new Set([{ foo: 1 }, { bar: 1 }])
  1074. await nextTick()
  1075. // without looseEqual, here is different from Array
  1076. expect(foo.selected).toEqual(false)
  1077. expect(bar.selected).toEqual(false)
  1078. })
  1079. it('should work with composition session', async () => {
  1080. const component = defineComponent({
  1081. data() {
  1082. return { value: '' }
  1083. },
  1084. render() {
  1085. return [
  1086. withVModel(
  1087. h('input', {
  1088. 'onUpdate:modelValue': setValue.bind(this),
  1089. }),
  1090. this.value,
  1091. ),
  1092. ]
  1093. },
  1094. })
  1095. render(h(component), root)
  1096. const input = root.querySelector('input')!
  1097. const data = root._vnode.component.data
  1098. expect(input.value).toEqual('')
  1099. //developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event
  1100. //compositionstart event could be fired after a user starts entering a Chinese character using a Pinyin IME
  1101. input.value = '使用拼音'
  1102. triggerEvent('compositionstart', input)
  1103. await nextTick()
  1104. expect(data.value).toEqual('')
  1105. // input event has no effect during composition session
  1106. input.value = '使用拼音输入'
  1107. triggerEvent('input', input)
  1108. await nextTick()
  1109. expect(data.value).toEqual('')
  1110. // After compositionend event being fired, an input event will be automatically trigger
  1111. triggerEvent('compositionend', input)
  1112. await nextTick()
  1113. expect(data.value).toEqual('使用拼音输入')
  1114. })
  1115. it('multiple select (model is number, option value is string)', async () => {
  1116. const component = defineComponent({
  1117. data() {
  1118. return {
  1119. value: [1, 2],
  1120. }
  1121. },
  1122. render() {
  1123. return [
  1124. withVModel(
  1125. h(
  1126. 'select',
  1127. {
  1128. multiple: true,
  1129. 'onUpdate:modelValue': setValue.bind(this),
  1130. },
  1131. [h('option', { value: '1' }), h('option', { value: '2' })],
  1132. ),
  1133. this.value,
  1134. ),
  1135. ]
  1136. },
  1137. })
  1138. render(h(component), root)
  1139. await nextTick()
  1140. const [foo, bar] = root.querySelectorAll('option')
  1141. expect(foo.selected).toEqual(true)
  1142. expect(bar.selected).toEqual(true)
  1143. })
  1144. // #10503
  1145. test('equal value with a leading 0 should trigger update.', async () => {
  1146. const setNum = function (this: any, value: any) {
  1147. this.num = value
  1148. }
  1149. const component = defineComponent({
  1150. data() {
  1151. return { num: 0 }
  1152. },
  1153. render() {
  1154. return [
  1155. withVModel(
  1156. h('input', {
  1157. id: 'input_num1',
  1158. type: 'number',
  1159. 'onUpdate:modelValue': setNum.bind(this),
  1160. }),
  1161. this.num,
  1162. ),
  1163. ]
  1164. },
  1165. })
  1166. render(h(component), root)
  1167. const data = root._vnode.component.data
  1168. const inputNum1 = root.querySelector('#input_num1')!
  1169. expect(inputNum1.value).toBe('0')
  1170. inputNum1.value = '01'
  1171. triggerEvent('input', inputNum1)
  1172. await nextTick()
  1173. expect(data.num).toBe(1)
  1174. expect(inputNum1.value).toBe('1')
  1175. })
  1176. })