vModel.spec.ts 25 KB

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