vModel.spec.ts 29 KB

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