vModel.spec.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  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: null }
  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. input.checked = true
  268. triggerEvent('change', input)
  269. await nextTick()
  270. expect(data.value).toEqual('yes')
  271. data.value = 'no'
  272. await nextTick()
  273. expect(input.checked).toEqual(false)
  274. data.value = 'yes'
  275. await nextTick()
  276. expect(input.checked).toEqual(true)
  277. input.checked = false
  278. triggerEvent('change', input)
  279. await nextTick()
  280. expect(data.value).toEqual('no')
  281. })
  282. it('should work with checkbox and true-value/false-value with object values', async () => {
  283. const component = defineComponent({
  284. data() {
  285. return { value: null }
  286. },
  287. render() {
  288. return [
  289. withVModel(
  290. h('input', {
  291. type: 'checkbox',
  292. 'true-value': { yes: 'yes' },
  293. 'false-value': { no: 'no' },
  294. 'onUpdate:modelValue': setValue.bind(this)
  295. }),
  296. this.value
  297. )
  298. ]
  299. }
  300. })
  301. render(h(component), root)
  302. const input = root.querySelector('input')
  303. const data = root._vnode.component.data
  304. input.checked = true
  305. triggerEvent('change', input)
  306. await nextTick()
  307. expect(data.value).toEqual({ yes: 'yes' })
  308. data.value = { no: 'no' }
  309. await nextTick()
  310. expect(input.checked).toEqual(false)
  311. data.value = { yes: 'yes' }
  312. await nextTick()
  313. expect(input.checked).toEqual(true)
  314. input.checked = false
  315. triggerEvent('change', input)
  316. await nextTick()
  317. expect(data.value).toEqual({ no: 'no' })
  318. })
  319. it(`should support array as a checkbox model`, async () => {
  320. const component = defineComponent({
  321. data() {
  322. return { value: [] }
  323. },
  324. render() {
  325. return [
  326. withVModel(
  327. h('input', {
  328. type: 'checkbox',
  329. class: 'foo',
  330. value: 'foo',
  331. 'onUpdate:modelValue': setValue.bind(this)
  332. }),
  333. this.value
  334. ),
  335. withVModel(
  336. h('input', {
  337. type: 'checkbox',
  338. class: 'bar',
  339. value: 'bar',
  340. 'onUpdate:modelValue': setValue.bind(this)
  341. }),
  342. this.value
  343. )
  344. ]
  345. }
  346. })
  347. render(h(component), root)
  348. const foo = root.querySelector('.foo')
  349. const bar = root.querySelector('.bar')
  350. const data = root._vnode.component.data
  351. foo.checked = true
  352. triggerEvent('change', foo)
  353. await nextTick()
  354. expect(data.value).toMatchObject(['foo'])
  355. bar.checked = true
  356. triggerEvent('change', bar)
  357. await nextTick()
  358. expect(data.value).toMatchObject(['foo', 'bar'])
  359. bar.checked = false
  360. triggerEvent('change', bar)
  361. await nextTick()
  362. expect(data.value).toMatchObject(['foo'])
  363. foo.checked = false
  364. triggerEvent('change', foo)
  365. await nextTick()
  366. expect(data.value).toMatchObject([])
  367. data.value = ['foo']
  368. await nextTick()
  369. expect(bar.checked).toEqual(false)
  370. expect(foo.checked).toEqual(true)
  371. data.value = ['bar']
  372. await nextTick()
  373. expect(foo.checked).toEqual(false)
  374. expect(bar.checked).toEqual(true)
  375. data.value = []
  376. await nextTick()
  377. expect(foo.checked).toEqual(false)
  378. expect(bar.checked).toEqual(false)
  379. })
  380. it(`should support Set as a checkbox model`, async () => {
  381. const component = defineComponent({
  382. data() {
  383. return { value: new Set() }
  384. },
  385. render() {
  386. return [
  387. withVModel(
  388. h('input', {
  389. type: 'checkbox',
  390. class: 'foo',
  391. value: 'foo',
  392. 'onUpdate:modelValue': setValue.bind(this)
  393. }),
  394. this.value
  395. ),
  396. withVModel(
  397. h('input', {
  398. type: 'checkbox',
  399. class: 'bar',
  400. value: 'bar',
  401. 'onUpdate:modelValue': setValue.bind(this)
  402. }),
  403. this.value
  404. )
  405. ]
  406. }
  407. })
  408. render(h(component), root)
  409. const foo = root.querySelector('.foo')
  410. const bar = root.querySelector('.bar')
  411. const data = root._vnode.component.data
  412. foo.checked = true
  413. triggerEvent('change', foo)
  414. await nextTick()
  415. expect(data.value).toMatchObject(new Set(['foo']))
  416. bar.checked = true
  417. triggerEvent('change', bar)
  418. await nextTick()
  419. expect(data.value).toMatchObject(new Set(['foo', 'bar']))
  420. bar.checked = false
  421. triggerEvent('change', bar)
  422. await nextTick()
  423. expect(data.value).toMatchObject(new Set(['foo']))
  424. foo.checked = false
  425. triggerEvent('change', foo)
  426. await nextTick()
  427. expect(data.value).toMatchObject(new Set())
  428. data.value = new Set(['foo'])
  429. await nextTick()
  430. expect(bar.checked).toEqual(false)
  431. expect(foo.checked).toEqual(true)
  432. data.value = new Set(['bar'])
  433. await nextTick()
  434. expect(foo.checked).toEqual(false)
  435. expect(bar.checked).toEqual(true)
  436. data.value = new Set()
  437. await nextTick()
  438. expect(foo.checked).toEqual(false)
  439. expect(bar.checked).toEqual(false)
  440. })
  441. it('should work with radio', async () => {
  442. const component = defineComponent({
  443. data() {
  444. return { value: null }
  445. },
  446. render() {
  447. return [
  448. withVModel(
  449. h('input', {
  450. type: 'radio',
  451. class: 'foo',
  452. value: 'foo',
  453. 'onUpdate:modelValue': setValue.bind(this)
  454. }),
  455. this.value
  456. ),
  457. withVModel(
  458. h('input', {
  459. type: 'radio',
  460. class: 'bar',
  461. value: 'bar',
  462. 'onUpdate:modelValue': setValue.bind(this)
  463. }),
  464. this.value
  465. )
  466. ]
  467. }
  468. })
  469. render(h(component), root)
  470. const foo = root.querySelector('.foo')
  471. const bar = root.querySelector('.bar')
  472. const data = root._vnode.component.data
  473. foo.checked = true
  474. triggerEvent('change', foo)
  475. await nextTick()
  476. expect(data.value).toEqual('foo')
  477. bar.checked = true
  478. triggerEvent('change', bar)
  479. await nextTick()
  480. expect(data.value).toEqual('bar')
  481. data.value = null
  482. await nextTick()
  483. expect(foo.checked).toEqual(false)
  484. expect(bar.checked).toEqual(false)
  485. data.value = 'foo'
  486. await nextTick()
  487. expect(foo.checked).toEqual(true)
  488. expect(bar.checked).toEqual(false)
  489. data.value = 'bar'
  490. await nextTick()
  491. expect(foo.checked).toEqual(false)
  492. expect(bar.checked).toEqual(true)
  493. })
  494. it('should work with single select', async () => {
  495. const component = defineComponent({
  496. data() {
  497. return { value: null }
  498. },
  499. render() {
  500. return [
  501. withVModel(
  502. h(
  503. 'select',
  504. {
  505. value: null,
  506. 'onUpdate:modelValue': setValue.bind(this)
  507. },
  508. [h('option', { value: 'foo' }), h('option', { value: 'bar' })]
  509. ),
  510. this.value
  511. )
  512. ]
  513. }
  514. })
  515. render(h(component), root)
  516. const input = root.querySelector('select')
  517. const foo = root.querySelector('option[value=foo]')
  518. const bar = root.querySelector('option[value=bar]')
  519. const data = root._vnode.component.data
  520. foo.selected = true
  521. triggerEvent('change', input)
  522. await nextTick()
  523. expect(data.value).toEqual('foo')
  524. foo.selected = false
  525. bar.selected = true
  526. triggerEvent('change', input)
  527. await nextTick()
  528. expect(data.value).toEqual('bar')
  529. foo.selected = false
  530. bar.selected = false
  531. data.value = 'foo'
  532. await nextTick()
  533. expect(input.value).toEqual('foo')
  534. expect(foo.selected).toEqual(true)
  535. expect(bar.selected).toEqual(false)
  536. foo.selected = true
  537. bar.selected = false
  538. data.value = 'bar'
  539. await nextTick()
  540. expect(input.value).toEqual('bar')
  541. expect(foo.selected).toEqual(false)
  542. expect(bar.selected).toEqual(true)
  543. })
  544. it('multiple select (model is Array)', async () => {
  545. const component = defineComponent({
  546. data() {
  547. return { value: [] }
  548. },
  549. render() {
  550. return [
  551. withVModel(
  552. h(
  553. 'select',
  554. {
  555. value: null,
  556. multiple: true,
  557. 'onUpdate:modelValue': setValue.bind(this)
  558. },
  559. [h('option', { value: 'foo' }), h('option', { value: 'bar' })]
  560. ),
  561. this.value
  562. )
  563. ]
  564. }
  565. })
  566. render(h(component), root)
  567. const input = root.querySelector('select')
  568. const foo = root.querySelector('option[value=foo]')
  569. const bar = root.querySelector('option[value=bar]')
  570. const data = root._vnode.component.data
  571. foo.selected = true
  572. triggerEvent('change', input)
  573. await nextTick()
  574. expect(data.value).toMatchObject(['foo'])
  575. foo.selected = false
  576. bar.selected = true
  577. triggerEvent('change', input)
  578. await nextTick()
  579. expect(data.value).toMatchObject(['bar'])
  580. foo.selected = true
  581. bar.selected = true
  582. triggerEvent('change', input)
  583. await nextTick()
  584. expect(data.value).toMatchObject(['foo', 'bar'])
  585. foo.selected = false
  586. bar.selected = false
  587. data.value = ['foo']
  588. await nextTick()
  589. expect(input.value).toEqual('foo')
  590. expect(foo.selected).toEqual(true)
  591. expect(bar.selected).toEqual(false)
  592. foo.selected = false
  593. bar.selected = false
  594. data.value = ['foo', 'bar']
  595. await nextTick()
  596. expect(foo.selected).toEqual(true)
  597. expect(bar.selected).toEqual(true)
  598. })
  599. it('v-model.number should work with select tag', async () => {
  600. const component = defineComponent({
  601. data() {
  602. return { value: null }
  603. },
  604. render() {
  605. return [
  606. withVModel(
  607. h(
  608. 'select',
  609. {
  610. value: null,
  611. 'onUpdate:modelValue': setValue.bind(this)
  612. },
  613. [h('option', { value: '1' }), h('option', { value: '2' })]
  614. ),
  615. this.value,
  616. {
  617. number: true
  618. }
  619. )
  620. ]
  621. }
  622. })
  623. render(h(component), root)
  624. const input = root.querySelector('select')
  625. const one = root.querySelector('option[value="1"]')
  626. const data = root._vnode.component.data
  627. one.selected = true
  628. triggerEvent('change', input)
  629. await nextTick()
  630. expect(typeof data.value).toEqual('number')
  631. expect(data.value).toEqual(1)
  632. })
  633. it('v-model.number should work with multiple select', async () => {
  634. const component = defineComponent({
  635. data() {
  636. return { value: [] }
  637. },
  638. render() {
  639. return [
  640. withVModel(
  641. h(
  642. 'select',
  643. {
  644. value: null,
  645. multiple: true,
  646. 'onUpdate:modelValue': setValue.bind(this)
  647. },
  648. [h('option', { value: '1' }), h('option', { value: '2' })]
  649. ),
  650. this.value,
  651. {
  652. number: true
  653. }
  654. )
  655. ]
  656. }
  657. })
  658. render(h(component), root)
  659. const input = root.querySelector('select')
  660. const one = root.querySelector('option[value="1"]')
  661. const two = root.querySelector('option[value="2"]')
  662. const data = root._vnode.component.data
  663. one.selected = true
  664. two.selected = false
  665. triggerEvent('change', input)
  666. await nextTick()
  667. expect(data.value).toMatchObject([1])
  668. one.selected = false
  669. two.selected = true
  670. triggerEvent('change', input)
  671. await nextTick()
  672. expect(data.value).toMatchObject([2])
  673. one.selected = true
  674. two.selected = true
  675. triggerEvent('change', input)
  676. await nextTick()
  677. expect(data.value).toMatchObject([1, 2])
  678. one.selected = false
  679. two.selected = false
  680. data.value = [1]
  681. await nextTick()
  682. expect(one.selected).toEqual(true)
  683. expect(two.selected).toEqual(false)
  684. one.selected = false
  685. two.selected = false
  686. data.value = [1, 2]
  687. await nextTick()
  688. expect(one.selected).toEqual(true)
  689. expect(two.selected).toEqual(true)
  690. })
  691. it('multiple select (model is Array, option value is object)', async () => {
  692. const fooValue = { foo: 1 }
  693. const barValue = { bar: 1 }
  694. const component = defineComponent({
  695. data() {
  696. return { value: [] }
  697. },
  698. render() {
  699. return [
  700. withVModel(
  701. h(
  702. 'select',
  703. {
  704. value: null,
  705. multiple: true,
  706. 'onUpdate:modelValue': setValue.bind(this)
  707. },
  708. [
  709. h('option', { value: fooValue }),
  710. h('option', { value: barValue })
  711. ]
  712. ),
  713. this.value
  714. )
  715. ]
  716. }
  717. })
  718. render(h(component), root)
  719. await nextTick()
  720. const input = root.querySelector('select')
  721. const [foo, bar] = root.querySelectorAll('option')
  722. const data = root._vnode.component.data
  723. foo.selected = true
  724. triggerEvent('change', input)
  725. await nextTick()
  726. expect(data.value).toMatchObject([fooValue])
  727. foo.selected = false
  728. bar.selected = true
  729. triggerEvent('change', input)
  730. await nextTick()
  731. expect(data.value).toMatchObject([barValue])
  732. foo.selected = true
  733. bar.selected = true
  734. triggerEvent('change', input)
  735. await nextTick()
  736. expect(data.value).toMatchObject([fooValue, barValue])
  737. foo.selected = false
  738. bar.selected = false
  739. data.value = [fooValue, barValue]
  740. await nextTick()
  741. expect(foo.selected).toEqual(true)
  742. expect(bar.selected).toEqual(true)
  743. foo.selected = false
  744. bar.selected = false
  745. data.value = [{ foo: 1 }, { bar: 1 }]
  746. await nextTick()
  747. // looseEqual
  748. expect(foo.selected).toEqual(true)
  749. expect(bar.selected).toEqual(true)
  750. })
  751. it('multiple select (model is Set)', async () => {
  752. const component = defineComponent({
  753. data() {
  754. return { value: new Set() }
  755. },
  756. render() {
  757. return [
  758. withVModel(
  759. h(
  760. 'select',
  761. {
  762. value: null,
  763. multiple: true,
  764. 'onUpdate:modelValue': setValue.bind(this)
  765. },
  766. [h('option', { value: 'foo' }), h('option', { value: 'bar' })]
  767. ),
  768. this.value
  769. )
  770. ]
  771. }
  772. })
  773. render(h(component), root)
  774. const input = root.querySelector('select')
  775. const foo = root.querySelector('option[value=foo]')
  776. const bar = root.querySelector('option[value=bar]')
  777. const data = root._vnode.component.data
  778. foo.selected = true
  779. triggerEvent('change', input)
  780. await nextTick()
  781. expect(data.value).toBeInstanceOf(Set)
  782. expect(data.value).toMatchObject(new Set(['foo']))
  783. foo.selected = false
  784. bar.selected = true
  785. triggerEvent('change', input)
  786. await nextTick()
  787. expect(data.value).toBeInstanceOf(Set)
  788. expect(data.value).toMatchObject(new Set(['bar']))
  789. foo.selected = true
  790. bar.selected = true
  791. triggerEvent('change', input)
  792. await nextTick()
  793. expect(data.value).toBeInstanceOf(Set)
  794. expect(data.value).toMatchObject(new Set(['foo', 'bar']))
  795. foo.selected = false
  796. bar.selected = false
  797. data.value = new Set(['foo'])
  798. await nextTick()
  799. expect(input.value).toEqual('foo')
  800. expect(foo.selected).toEqual(true)
  801. expect(bar.selected).toEqual(false)
  802. foo.selected = false
  803. bar.selected = false
  804. data.value = new Set(['foo', 'bar'])
  805. await nextTick()
  806. expect(foo.selected).toEqual(true)
  807. expect(bar.selected).toEqual(true)
  808. })
  809. it('multiple select (model is Set, option value is object)', async () => {
  810. const fooValue = { foo: 1 }
  811. const barValue = { bar: 1 }
  812. const component = defineComponent({
  813. data() {
  814. return { value: new Set() }
  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. [
  827. h('option', { value: fooValue }),
  828. h('option', { value: barValue })
  829. ]
  830. ),
  831. this.value
  832. )
  833. ]
  834. }
  835. })
  836. render(h(component), root)
  837. await nextTick()
  838. const input = root.querySelector('select')
  839. const [foo, bar] = root.querySelectorAll('option')
  840. const data = root._vnode.component.data
  841. foo.selected = true
  842. triggerEvent('change', input)
  843. await nextTick()
  844. expect(data.value).toMatchObject(new Set([fooValue]))
  845. foo.selected = false
  846. bar.selected = true
  847. triggerEvent('change', input)
  848. await nextTick()
  849. expect(data.value).toMatchObject(new Set([barValue]))
  850. foo.selected = true
  851. bar.selected = true
  852. triggerEvent('change', input)
  853. await nextTick()
  854. expect(data.value).toMatchObject(new Set([fooValue, barValue]))
  855. foo.selected = false
  856. bar.selected = false
  857. data.value = new Set([fooValue, barValue])
  858. await nextTick()
  859. expect(foo.selected).toEqual(true)
  860. expect(bar.selected).toEqual(true)
  861. foo.selected = false
  862. bar.selected = false
  863. data.value = new Set([{ foo: 1 }, { bar: 1 }])
  864. await nextTick()
  865. // whithout looseEqual, here is different from Array
  866. expect(foo.selected).toEqual(false)
  867. expect(bar.selected).toEqual(false)
  868. })
  869. it('should work with composition session', async () => {
  870. const component = defineComponent({
  871. data() {
  872. return { value: '' }
  873. },
  874. render() {
  875. return [
  876. withVModel(
  877. h('input', {
  878. 'onUpdate:modelValue': setValue.bind(this)
  879. }),
  880. this.value
  881. )
  882. ]
  883. }
  884. })
  885. render(h(component), root)
  886. const input = root.querySelector('input')!
  887. const data = root._vnode.component.data
  888. expect(input.value).toEqual('')
  889. //developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event
  890. //compositionstart event could be fired after a user starts entering a Chinese character using a Pinyin IME
  891. input.value = '使用拼音'
  892. triggerEvent('compositionstart', input)
  893. await nextTick()
  894. expect(data.value).toEqual('')
  895. // input event has no effect during composition session
  896. input.value = '使用拼音输入'
  897. triggerEvent('input', input)
  898. await nextTick()
  899. expect(data.value).toEqual('')
  900. // After compositionend event being fired, an input event will be automatically trigger
  901. triggerEvent('compositionend', input)
  902. await nextTick()
  903. expect(data.value).toEqual('使用拼音输入')
  904. })
  905. })