vModel.spec.ts 29 KB

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