vModel.spec.ts 30 KB

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