vModel.spec.ts 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545
  1. import {
  2. type VNode,
  3. defineComponent,
  4. h,
  5. nextTick,
  6. ref,
  7. render,
  8. vModelCheckbox,
  9. vModelDynamic,
  10. withDirectives,
  11. } from '@vue/runtime-dom'
  12. const triggerEvent = (type: string, el: Element) => {
  13. const event = new Event(type)
  14. el.dispatchEvent(event)
  15. }
  16. const withVModel = (node: VNode, arg: any, mods?: any) =>
  17. withDirectives(node, [[vModelDynamic, arg, '', mods]])
  18. const setValue = function (this: any, value: any) {
  19. this.value = value
  20. }
  21. let root: any
  22. beforeEach(() => {
  23. root = document.createElement('div') as any
  24. })
  25. describe('vModel', () => {
  26. it('should work with text input', async () => {
  27. const manualListener = vi.fn()
  28. const component = defineComponent({
  29. data() {
  30. return { value: null }
  31. },
  32. render() {
  33. return [
  34. withVModel(
  35. h('input', {
  36. 'onUpdate:modelValue': setValue.bind(this),
  37. onInput: () => {
  38. manualListener(data.value)
  39. },
  40. }),
  41. this.value,
  42. ),
  43. ]
  44. },
  45. })
  46. render(h(component), root)
  47. const input = root.querySelector('input')!
  48. const data = root._vnode.component.data
  49. expect(input.value).toEqual('')
  50. input.value = 'foo'
  51. triggerEvent('input', input)
  52. await nextTick()
  53. expect(data.value).toEqual('foo')
  54. // #1931
  55. expect(manualListener).toHaveBeenCalledWith('foo')
  56. data.value = 'bar'
  57. await nextTick()
  58. expect(input.value).toEqual('bar')
  59. data.value = undefined
  60. await nextTick()
  61. expect(input.value).toEqual('')
  62. })
  63. it('should work with number input', async () => {
  64. const component = defineComponent({
  65. data() {
  66. return { value: null }
  67. },
  68. render() {
  69. return [
  70. withVModel(
  71. h('input', {
  72. type: 'number',
  73. 'onUpdate:modelValue': setValue.bind(this),
  74. }),
  75. this.value,
  76. ),
  77. ]
  78. },
  79. })
  80. render(h(component), root)
  81. const input = root.querySelector('input')!
  82. const data = root._vnode.component.data
  83. expect(input.value).toEqual('')
  84. expect(input.type).toEqual('number')
  85. input.value = 1
  86. triggerEvent('input', input)
  87. await nextTick()
  88. expect(typeof data.value).toEqual('number')
  89. expect(data.value).toEqual(1)
  90. })
  91. // #7003
  92. it('should work with number input and be able to update rendering correctly', async () => {
  93. const setValue1 = function (this: any, value: any) {
  94. this.value1 = value
  95. }
  96. const setValue2 = function (this: any, value: any) {
  97. this.value2 = value
  98. }
  99. const component = defineComponent({
  100. data() {
  101. return { value1: 1.002, value2: 1.002 }
  102. },
  103. render() {
  104. return [
  105. withVModel(
  106. h('input', {
  107. id: 'input_num1',
  108. type: 'number',
  109. 'onUpdate:modelValue': setValue1.bind(this),
  110. }),
  111. this.value1,
  112. ),
  113. withVModel(
  114. h('input', {
  115. id: 'input_num2',
  116. type: 'number',
  117. 'onUpdate:modelValue': setValue2.bind(this),
  118. }),
  119. this.value2,
  120. ),
  121. ]
  122. },
  123. })
  124. render(h(component), root)
  125. const data = root._vnode.component.data
  126. const inputNum1 = root.querySelector('#input_num1')!
  127. expect(inputNum1.value).toBe('1.002')
  128. const inputNum2 = root.querySelector('#input_num2')!
  129. expect(inputNum2.value).toBe('1.002')
  130. inputNum1.value = '1.00'
  131. triggerEvent('input', inputNum1)
  132. await nextTick()
  133. expect(data.value1).toBe(1)
  134. inputNum2.value = '1.00'
  135. triggerEvent('input', inputNum2)
  136. await nextTick()
  137. expect(data.value2).toBe(1)
  138. expect(inputNum1.value).toBe('1.00')
  139. })
  140. it('should work with multiple listeners', async () => {
  141. const spy = vi.fn()
  142. const component = defineComponent({
  143. data() {
  144. return { value: null }
  145. },
  146. render() {
  147. return [
  148. withVModel(
  149. h('input', {
  150. 'onUpdate:modelValue': [setValue.bind(this), spy],
  151. }),
  152. this.value,
  153. ),
  154. ]
  155. },
  156. })
  157. render(h(component), root)
  158. const input = root.querySelector('input')!
  159. const data = root._vnode.component.data
  160. input.value = 'foo'
  161. triggerEvent('input', input)
  162. await nextTick()
  163. expect(data.value).toEqual('foo')
  164. expect(spy).toHaveBeenCalledWith('foo')
  165. })
  166. it('should work with updated listeners', async () => {
  167. const spy1 = vi.fn()
  168. const spy2 = vi.fn()
  169. const toggle = ref(true)
  170. const component = defineComponent({
  171. render() {
  172. return [
  173. withVModel(
  174. h('input', {
  175. 'onUpdate:modelValue': toggle.value ? spy1 : spy2,
  176. }),
  177. 'foo',
  178. ),
  179. ]
  180. },
  181. })
  182. render(h(component), root)
  183. const input = root.querySelector('input')!
  184. input.value = 'foo'
  185. triggerEvent('input', input)
  186. await nextTick()
  187. expect(spy1).toHaveBeenCalledWith('foo')
  188. // update listener
  189. toggle.value = false
  190. await nextTick()
  191. input.value = 'bar'
  192. triggerEvent('input', input)
  193. await nextTick()
  194. expect(spy1).not.toHaveBeenCalledWith('bar')
  195. expect(spy2).toHaveBeenCalledWith('bar')
  196. })
  197. it('should work with textarea', async () => {
  198. const component = defineComponent({
  199. data() {
  200. return { value: null }
  201. },
  202. render() {
  203. return [
  204. withVModel(
  205. h('textarea', {
  206. 'onUpdate:modelValue': setValue.bind(this),
  207. }),
  208. this.value,
  209. ),
  210. ]
  211. },
  212. })
  213. render(h(component), root)
  214. const input = root.querySelector('textarea')
  215. const data = root._vnode.component.data
  216. input.value = 'foo'
  217. triggerEvent('input', input)
  218. await nextTick()
  219. expect(data.value).toEqual('foo')
  220. data.value = 'bar'
  221. await nextTick()
  222. expect(input.value).toEqual('bar')
  223. })
  224. it('should support modifiers', async () => {
  225. const component = defineComponent({
  226. data() {
  227. return {
  228. number: null,
  229. trim: null,
  230. lazy: null,
  231. trimNumber: null,
  232. trimLazy: null,
  233. }
  234. },
  235. render() {
  236. return [
  237. withVModel(
  238. h('input', {
  239. class: 'number',
  240. 'onUpdate:modelValue': (val: any) => {
  241. this.number = val
  242. },
  243. }),
  244. this.number,
  245. {
  246. number: true,
  247. },
  248. ),
  249. withVModel(
  250. h('input', {
  251. class: 'trim',
  252. 'onUpdate:modelValue': (val: any) => {
  253. this.trim = val
  254. },
  255. }),
  256. this.trim,
  257. {
  258. trim: true,
  259. },
  260. ),
  261. withVModel(
  262. h('input', {
  263. class: 'trim-lazy',
  264. 'onUpdate:modelValue': (val: any) => {
  265. this.trimLazy = val
  266. },
  267. }),
  268. this.trim,
  269. {
  270. trim: true,
  271. lazy: true,
  272. },
  273. ),
  274. withVModel(
  275. h('input', {
  276. class: 'trim-number',
  277. 'onUpdate:modelValue': (val: any) => {
  278. this.trimNumber = val
  279. },
  280. }),
  281. this.trimNumber,
  282. {
  283. trim: true,
  284. number: true,
  285. },
  286. ),
  287. withVModel(
  288. h('input', {
  289. class: 'lazy',
  290. 'onUpdate:modelValue': (val: any) => {
  291. this.lazy = val
  292. },
  293. }),
  294. this.lazy,
  295. {
  296. lazy: true,
  297. },
  298. ),
  299. ]
  300. },
  301. })
  302. render(h(component), root)
  303. const number = root.querySelector('.number')
  304. const trim = root.querySelector('.trim')
  305. const trimNumber = root.querySelector('.trim-number')
  306. const trimLazy = root.querySelector('.trim-lazy')
  307. const lazy = root.querySelector('.lazy')
  308. const data = root._vnode.component.data
  309. number.value = '+01.2'
  310. triggerEvent('input', number)
  311. await nextTick()
  312. expect(data.number).toEqual(1.2)
  313. triggerEvent('change', number)
  314. await nextTick()
  315. expect(number.value).toEqual('1.2')
  316. trim.value = ' hello, world '
  317. triggerEvent('input', trim)
  318. await nextTick()
  319. expect(data.trim).toEqual('hello, world')
  320. trimNumber.value = ' 1 '
  321. triggerEvent('input', trimNumber)
  322. await nextTick()
  323. expect(data.trimNumber).toEqual(1)
  324. trimNumber.value = ' +01.2 '
  325. triggerEvent('input', trimNumber)
  326. await nextTick()
  327. expect(data.trimNumber).toEqual(1.2)
  328. trimLazy.value = ' ddd '
  329. triggerEvent('change', trimLazy)
  330. await nextTick()
  331. expect(data.trimLazy).toEqual('ddd')
  332. lazy.value = 'foo'
  333. triggerEvent('change', lazy)
  334. await nextTick()
  335. expect(data.lazy).toEqual('foo')
  336. })
  337. it('should preserve unresolved trimmed text while focused in nested shadow roots', async () => {
  338. const model = ref('')
  339. const component = defineComponent({
  340. render() {
  341. return withVModel(
  342. h('input', {
  343. 'onUpdate:modelValue': (value: string) => {
  344. model.value = value
  345. },
  346. }),
  347. model.value,
  348. {
  349. trim: true,
  350. },
  351. )
  352. },
  353. })
  354. document.body.appendChild(root)
  355. const outerShadowRoot = root.attachShadow({ mode: 'open' })
  356. const innerHost = document.createElement('div')
  357. outerShadowRoot.appendChild(innerHost)
  358. const innerShadowRoot = innerHost.attachShadow({ mode: 'open' })
  359. try {
  360. render(h(component), innerShadowRoot)
  361. const input = innerShadowRoot.querySelector('input') as HTMLInputElement
  362. input.focus()
  363. expect(document.activeElement).toBe(root)
  364. expect(outerShadowRoot.activeElement).toBe(innerHost)
  365. expect(innerShadowRoot.activeElement).toBe(input)
  366. input.value = ' hello, world '
  367. triggerEvent('input', input)
  368. await nextTick()
  369. expect(model.value).toEqual('hello, world')
  370. expect(input.value).toEqual(' hello, world ')
  371. } finally {
  372. render(null, innerShadowRoot)
  373. root.remove()
  374. }
  375. })
  376. it('should work with range', async () => {
  377. const component = defineComponent({
  378. data() {
  379. return { value: 25 }
  380. },
  381. render() {
  382. return [
  383. withVModel(
  384. h('input', {
  385. type: 'range',
  386. min: 1,
  387. max: 100,
  388. class: 'foo',
  389. 'onUpdate:modelValue': setValue.bind(this),
  390. }),
  391. this.value,
  392. {
  393. number: true,
  394. },
  395. ),
  396. withVModel(
  397. h('input', {
  398. type: 'range',
  399. min: 1,
  400. max: 100,
  401. class: 'bar',
  402. 'onUpdate:modelValue': setValue.bind(this),
  403. }),
  404. this.value,
  405. {
  406. lazy: true,
  407. },
  408. ),
  409. ]
  410. },
  411. })
  412. render(h(component), root)
  413. const foo = root.querySelector('.foo')
  414. const bar = root.querySelector('.bar')
  415. const data = root._vnode.component.data
  416. foo.value = 20
  417. triggerEvent('input', foo)
  418. await nextTick()
  419. expect(data.value).toEqual(20)
  420. foo.value = 200
  421. triggerEvent('input', foo)
  422. await nextTick()
  423. expect(data.value).toEqual(100)
  424. foo.value = -1
  425. triggerEvent('input', foo)
  426. await nextTick()
  427. expect(data.value).toEqual(1)
  428. bar.value = 30
  429. triggerEvent('change', bar)
  430. await nextTick()
  431. expect(data.value).toEqual('30')
  432. bar.value = 200
  433. triggerEvent('change', bar)
  434. await nextTick()
  435. expect(data.value).toEqual('100')
  436. bar.value = -1
  437. triggerEvent('change', bar)
  438. await nextTick()
  439. expect(data.value).toEqual('1')
  440. data.value = 60
  441. await nextTick()
  442. expect(foo.value).toEqual('60')
  443. expect(bar.value).toEqual('60')
  444. data.value = -1
  445. await nextTick()
  446. expect(foo.value).toEqual('1')
  447. expect(bar.value).toEqual('1')
  448. data.value = 200
  449. await nextTick()
  450. expect(foo.value).toEqual('100')
  451. expect(bar.value).toEqual('100')
  452. })
  453. it('should work with checkbox', async () => {
  454. const component = defineComponent({
  455. data() {
  456. return { value: null }
  457. },
  458. render() {
  459. return [
  460. withVModel(
  461. h('input', {
  462. type: 'checkbox',
  463. 'onUpdate:modelValue': setValue.bind(this),
  464. }),
  465. this.value,
  466. ),
  467. ]
  468. },
  469. })
  470. render(h(component), root)
  471. const input = root.querySelector('input')
  472. const data = root._vnode.component.data
  473. input.checked = true
  474. triggerEvent('change', input)
  475. await nextTick()
  476. expect(data.value).toEqual(true)
  477. data.value = false
  478. await nextTick()
  479. expect(input.checked).toEqual(false)
  480. data.value = true
  481. await nextTick()
  482. expect(input.checked).toEqual(true)
  483. input.checked = false
  484. triggerEvent('change', input)
  485. await nextTick()
  486. expect(data.value).toEqual(false)
  487. })
  488. it('should work with checkbox and true-value/false-value', async () => {
  489. const component = defineComponent({
  490. data() {
  491. return { value: 'yes' }
  492. },
  493. render() {
  494. return [
  495. withVModel(
  496. h('input', {
  497. type: 'checkbox',
  498. 'true-value': 'yes',
  499. 'false-value': 'no',
  500. 'onUpdate:modelValue': setValue.bind(this),
  501. }),
  502. this.value,
  503. ),
  504. ]
  505. },
  506. })
  507. render(h(component), root)
  508. const input = root.querySelector('input')
  509. const data = root._vnode.component.data
  510. // DOM checked state should respect initial true-value/false-value
  511. expect(input.checked).toEqual(true)
  512. input.checked = false
  513. triggerEvent('change', input)
  514. await nextTick()
  515. expect(data.value).toEqual('no')
  516. data.value = 'yes'
  517. await nextTick()
  518. expect(input.checked).toEqual(true)
  519. data.value = 'no'
  520. await nextTick()
  521. expect(input.checked).toEqual(false)
  522. input.checked = true
  523. triggerEvent('change', input)
  524. await nextTick()
  525. expect(data.value).toEqual('yes')
  526. })
  527. it('should work with checkbox and true-value/false-value with object values', async () => {
  528. const component = defineComponent({
  529. data() {
  530. return { value: null }
  531. },
  532. render() {
  533. return [
  534. withVModel(
  535. h('input', {
  536. type: 'checkbox',
  537. 'true-value': { yes: 'yes' },
  538. 'false-value': { no: 'no' },
  539. 'onUpdate:modelValue': setValue.bind(this),
  540. }),
  541. this.value,
  542. ),
  543. ]
  544. },
  545. })
  546. render(h(component), root)
  547. const input = root.querySelector('input')
  548. const data = root._vnode.component.data
  549. input.checked = true
  550. triggerEvent('change', input)
  551. await nextTick()
  552. expect(data.value).toEqual({ yes: 'yes' })
  553. data.value = { no: 'no' }
  554. await nextTick()
  555. expect(input.checked).toEqual(false)
  556. data.value = { yes: 'yes' }
  557. await nextTick()
  558. expect(input.checked).toEqual(true)
  559. input.checked = false
  560. triggerEvent('change', input)
  561. await nextTick()
  562. expect(data.value).toEqual({ no: 'no' })
  563. })
  564. it(`should support array as a checkbox model`, async () => {
  565. const component = defineComponent({
  566. data() {
  567. return { value: [] }
  568. },
  569. render() {
  570. return [
  571. withVModel(
  572. h('input', {
  573. type: 'checkbox',
  574. class: 'foo',
  575. value: 'foo',
  576. 'onUpdate:modelValue': setValue.bind(this),
  577. }),
  578. this.value,
  579. ),
  580. withVModel(
  581. h('input', {
  582. type: 'checkbox',
  583. class: 'bar',
  584. value: 'bar',
  585. 'onUpdate:modelValue': setValue.bind(this),
  586. }),
  587. this.value,
  588. ),
  589. ]
  590. },
  591. })
  592. render(h(component), root)
  593. const foo = root.querySelector('.foo')
  594. const bar = root.querySelector('.bar')
  595. const data = root._vnode.component.data
  596. foo.checked = true
  597. triggerEvent('change', foo)
  598. await nextTick()
  599. expect(data.value).toMatchObject(['foo'])
  600. bar.checked = true
  601. triggerEvent('change', bar)
  602. await nextTick()
  603. expect(data.value).toMatchObject(['foo', 'bar'])
  604. bar.checked = false
  605. triggerEvent('change', bar)
  606. await nextTick()
  607. expect(data.value).toMatchObject(['foo'])
  608. foo.checked = false
  609. triggerEvent('change', foo)
  610. await nextTick()
  611. expect(data.value).toMatchObject([])
  612. data.value = ['foo']
  613. await nextTick()
  614. expect(bar.checked).toEqual(false)
  615. expect(foo.checked).toEqual(true)
  616. data.value = ['bar']
  617. await nextTick()
  618. expect(foo.checked).toEqual(false)
  619. expect(bar.checked).toEqual(true)
  620. data.value = []
  621. await nextTick()
  622. expect(foo.checked).toEqual(false)
  623. expect(bar.checked).toEqual(false)
  624. })
  625. it(`should support Set as a checkbox model`, async () => {
  626. const component = defineComponent({
  627. data() {
  628. return { value: new Set() }
  629. },
  630. render() {
  631. return [
  632. withVModel(
  633. h('input', {
  634. type: 'checkbox',
  635. class: 'foo',
  636. value: 'foo',
  637. 'onUpdate:modelValue': setValue.bind(this),
  638. }),
  639. this.value,
  640. ),
  641. withVModel(
  642. h('input', {
  643. type: 'checkbox',
  644. class: 'bar',
  645. value: 'bar',
  646. 'onUpdate:modelValue': setValue.bind(this),
  647. }),
  648. this.value,
  649. ),
  650. ]
  651. },
  652. })
  653. render(h(component), root)
  654. const foo = root.querySelector('.foo')
  655. const bar = root.querySelector('.bar')
  656. const data = root._vnode.component.data
  657. foo.checked = true
  658. triggerEvent('change', foo)
  659. await nextTick()
  660. expect(data.value).toMatchObject(new Set(['foo']))
  661. bar.checked = true
  662. triggerEvent('change', bar)
  663. await nextTick()
  664. expect(data.value).toMatchObject(new Set(['foo', 'bar']))
  665. bar.checked = false
  666. triggerEvent('change', bar)
  667. await nextTick()
  668. expect(data.value).toMatchObject(new Set(['foo']))
  669. foo.checked = false
  670. triggerEvent('change', foo)
  671. await nextTick()
  672. expect(data.value).toMatchObject(new Set())
  673. data.value = new Set(['foo'])
  674. await nextTick()
  675. expect(bar.checked).toEqual(false)
  676. expect(foo.checked).toEqual(true)
  677. data.value = new Set(['bar'])
  678. await nextTick()
  679. expect(foo.checked).toEqual(false)
  680. expect(bar.checked).toEqual(true)
  681. data.value = new Set()
  682. await nextTick()
  683. expect(foo.checked).toEqual(false)
  684. expect(bar.checked).toEqual(false)
  685. })
  686. it('should not update DOM unnecessarily', async () => {
  687. const component = defineComponent({
  688. data() {
  689. return { value: true }
  690. },
  691. render() {
  692. return [
  693. withVModel(
  694. h('input', {
  695. type: 'checkbox',
  696. 'onUpdate:modelValue': setValue.bind(this),
  697. }),
  698. this.value,
  699. ),
  700. ]
  701. },
  702. })
  703. render(h(component), root)
  704. const input = root.querySelector('input')
  705. const data = root._vnode.component.data
  706. const setCheckedSpy = vi.spyOn(input, 'checked', 'set')
  707. // Trigger a change event without actually changing the value
  708. triggerEvent('change', input)
  709. await nextTick()
  710. expect(data.value).toEqual(true)
  711. expect(setCheckedSpy).not.toHaveBeenCalled()
  712. // Change the value and trigger a change event
  713. input.checked = false
  714. triggerEvent('change', input)
  715. await nextTick()
  716. expect(data.value).toEqual(false)
  717. expect(setCheckedSpy).toHaveBeenCalledTimes(1)
  718. setCheckedSpy.mockClear()
  719. data.value = false
  720. await nextTick()
  721. expect(input.checked).toEqual(false)
  722. expect(setCheckedSpy).not.toHaveBeenCalled()
  723. data.value = true
  724. await nextTick()
  725. expect(input.checked).toEqual(true)
  726. expect(setCheckedSpy).toHaveBeenCalledTimes(1)
  727. })
  728. it('should handle array values correctly without unnecessary updates', async () => {
  729. const component = defineComponent({
  730. data() {
  731. return { value: ['foo'] }
  732. },
  733. render() {
  734. return [
  735. withVModel(
  736. h('input', {
  737. type: 'checkbox',
  738. value: 'foo',
  739. 'onUpdate:modelValue': setValue.bind(this),
  740. }),
  741. this.value,
  742. ),
  743. withVModel(
  744. h('input', {
  745. type: 'checkbox',
  746. value: 'bar',
  747. 'onUpdate:modelValue': setValue.bind(this),
  748. }),
  749. this.value,
  750. ),
  751. ]
  752. },
  753. })
  754. render(h(component), root)
  755. const [foo, bar] = root.querySelectorAll('input')
  756. const data = root._vnode.component.data
  757. const setCheckedSpyFoo = vi.spyOn(foo, 'checked', 'set')
  758. const setCheckedSpyBar = vi.spyOn(bar, 'checked', 'set')
  759. expect(foo.checked).toEqual(true)
  760. expect(bar.checked).toEqual(false)
  761. triggerEvent('change', foo)
  762. await nextTick()
  763. expect(data.value).toEqual(['foo'])
  764. expect(setCheckedSpyFoo).not.toHaveBeenCalled()
  765. bar.checked = true
  766. triggerEvent('change', bar)
  767. await nextTick()
  768. expect(data.value).toEqual(['foo', 'bar'])
  769. expect(setCheckedSpyBar).toHaveBeenCalledTimes(1)
  770. setCheckedSpyFoo.mockClear()
  771. setCheckedSpyBar.mockClear()
  772. data.value = ['foo', 'bar']
  773. await nextTick()
  774. expect(setCheckedSpyFoo).not.toHaveBeenCalled()
  775. expect(setCheckedSpyBar).not.toHaveBeenCalled()
  776. data.value = ['bar']
  777. await nextTick()
  778. expect(setCheckedSpyFoo).toHaveBeenCalledTimes(1)
  779. expect(setCheckedSpyBar).not.toHaveBeenCalled()
  780. expect(foo.checked).toEqual(false)
  781. expect(bar.checked).toEqual(true)
  782. })
  783. it('should work with radio', async () => {
  784. const component = defineComponent({
  785. data() {
  786. return { value: null }
  787. },
  788. render() {
  789. return [
  790. withVModel(
  791. h('input', {
  792. type: 'radio',
  793. class: 'foo',
  794. value: 'foo',
  795. 'onUpdate:modelValue': setValue.bind(this),
  796. }),
  797. this.value,
  798. ),
  799. withVModel(
  800. h('input', {
  801. type: 'radio',
  802. class: 'bar',
  803. value: 'bar',
  804. 'onUpdate:modelValue': setValue.bind(this),
  805. }),
  806. this.value,
  807. ),
  808. ]
  809. },
  810. })
  811. render(h(component), root)
  812. const foo = root.querySelector('.foo')
  813. const bar = root.querySelector('.bar')
  814. const data = root._vnode.component.data
  815. foo.checked = true
  816. triggerEvent('change', foo)
  817. await nextTick()
  818. expect(data.value).toEqual('foo')
  819. bar.checked = true
  820. triggerEvent('change', bar)
  821. await nextTick()
  822. expect(data.value).toEqual('bar')
  823. data.value = null
  824. await nextTick()
  825. expect(foo.checked).toEqual(false)
  826. expect(bar.checked).toEqual(false)
  827. data.value = 'foo'
  828. await nextTick()
  829. expect(foo.checked).toEqual(true)
  830. expect(bar.checked).toEqual(false)
  831. data.value = 'bar'
  832. await nextTick()
  833. expect(foo.checked).toEqual(false)
  834. expect(bar.checked).toEqual(true)
  835. })
  836. it('should work with single select', async () => {
  837. const component = defineComponent({
  838. data() {
  839. return { value: null }
  840. },
  841. render() {
  842. return [
  843. withVModel(
  844. h(
  845. 'select',
  846. {
  847. value: null,
  848. 'onUpdate:modelValue': setValue.bind(this),
  849. },
  850. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  851. ),
  852. this.value,
  853. ),
  854. ]
  855. },
  856. })
  857. render(h(component), root)
  858. const input = root.querySelector('select')
  859. const foo = root.querySelector('option[value=foo]')
  860. const bar = root.querySelector('option[value=bar]')
  861. const data = root._vnode.component.data
  862. foo.selected = true
  863. triggerEvent('change', input)
  864. await nextTick()
  865. expect(data.value).toEqual('foo')
  866. foo.selected = false
  867. bar.selected = true
  868. triggerEvent('change', input)
  869. await nextTick()
  870. expect(data.value).toEqual('bar')
  871. foo.selected = false
  872. bar.selected = false
  873. data.value = 'foo'
  874. await nextTick()
  875. expect(input.value).toEqual('foo')
  876. expect(foo.selected).toEqual(true)
  877. expect(bar.selected).toEqual(false)
  878. foo.selected = true
  879. bar.selected = false
  880. data.value = 'bar'
  881. await nextTick()
  882. expect(input.value).toEqual('bar')
  883. expect(foo.selected).toEqual(false)
  884. expect(bar.selected).toEqual(true)
  885. })
  886. it('multiple select (model is Array)', async () => {
  887. const component = defineComponent({
  888. data() {
  889. return { value: [] }
  890. },
  891. render() {
  892. return [
  893. withVModel(
  894. h(
  895. 'select',
  896. {
  897. value: null,
  898. multiple: true,
  899. 'onUpdate:modelValue': setValue.bind(this),
  900. },
  901. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  902. ),
  903. this.value,
  904. ),
  905. ]
  906. },
  907. })
  908. render(h(component), root)
  909. const input = root.querySelector('select')
  910. const foo = root.querySelector('option[value=foo]')
  911. const bar = root.querySelector('option[value=bar]')
  912. const data = root._vnode.component.data
  913. foo.selected = true
  914. triggerEvent('change', input)
  915. await nextTick()
  916. expect(data.value).toMatchObject(['foo'])
  917. foo.selected = false
  918. bar.selected = true
  919. triggerEvent('change', input)
  920. await nextTick()
  921. expect(data.value).toMatchObject(['bar'])
  922. foo.selected = true
  923. bar.selected = true
  924. triggerEvent('change', input)
  925. await nextTick()
  926. expect(data.value).toMatchObject(['foo', 'bar'])
  927. foo.selected = false
  928. bar.selected = false
  929. data.value = ['foo']
  930. await nextTick()
  931. expect(input.value).toEqual('foo')
  932. expect(foo.selected).toEqual(true)
  933. expect(bar.selected).toEqual(false)
  934. foo.selected = false
  935. bar.selected = false
  936. data.value = ['foo', 'bar']
  937. await nextTick()
  938. expect(foo.selected).toEqual(true)
  939. expect(bar.selected).toEqual(true)
  940. })
  941. it('v-model.number should work with select tag', async () => {
  942. const component = defineComponent({
  943. data() {
  944. return { value: null }
  945. },
  946. render() {
  947. return [
  948. withVModel(
  949. h(
  950. 'select',
  951. {
  952. value: null,
  953. 'onUpdate:modelValue': setValue.bind(this),
  954. },
  955. [h('option', { value: '1' }), h('option', { value: '2' })],
  956. ),
  957. this.value,
  958. {
  959. number: true,
  960. },
  961. ),
  962. ]
  963. },
  964. })
  965. render(h(component), root)
  966. const input = root.querySelector('select')
  967. const one = root.querySelector('option[value="1"]')
  968. const data = root._vnode.component.data
  969. one.selected = true
  970. triggerEvent('change', input)
  971. await nextTick()
  972. expect(typeof data.value).toEqual('number')
  973. expect(data.value).toEqual(1)
  974. })
  975. it('v-model.number should work with multiple select', async () => {
  976. const component = defineComponent({
  977. data() {
  978. return { value: [] }
  979. },
  980. render() {
  981. return [
  982. withVModel(
  983. h(
  984. 'select',
  985. {
  986. value: null,
  987. multiple: true,
  988. 'onUpdate:modelValue': setValue.bind(this),
  989. },
  990. [h('option', { value: '1' }), h('option', { value: '2' })],
  991. ),
  992. this.value,
  993. {
  994. number: true,
  995. },
  996. ),
  997. ]
  998. },
  999. })
  1000. render(h(component), root)
  1001. const input = root.querySelector('select')
  1002. const one = root.querySelector('option[value="1"]')
  1003. const two = root.querySelector('option[value="2"]')
  1004. const data = root._vnode.component.data
  1005. one.selected = true
  1006. two.selected = false
  1007. triggerEvent('change', input)
  1008. await nextTick()
  1009. expect(data.value).toMatchObject([1])
  1010. one.selected = false
  1011. two.selected = true
  1012. triggerEvent('change', input)
  1013. await nextTick()
  1014. expect(data.value).toMatchObject([2])
  1015. one.selected = true
  1016. two.selected = true
  1017. triggerEvent('change', input)
  1018. await nextTick()
  1019. expect(data.value).toMatchObject([1, 2])
  1020. one.selected = false
  1021. two.selected = false
  1022. data.value = [1]
  1023. await nextTick()
  1024. expect(one.selected).toEqual(true)
  1025. expect(two.selected).toEqual(false)
  1026. one.selected = false
  1027. two.selected = false
  1028. data.value = [1, 2]
  1029. await nextTick()
  1030. expect(one.selected).toEqual(true)
  1031. expect(two.selected).toEqual(true)
  1032. })
  1033. it('multiple select (model is Array, option value is object)', async () => {
  1034. const fooValue = { foo: 1 }
  1035. const barValue = { bar: 1 }
  1036. const component = defineComponent({
  1037. data() {
  1038. return { value: [] }
  1039. },
  1040. render() {
  1041. return [
  1042. withVModel(
  1043. h(
  1044. 'select',
  1045. {
  1046. value: null,
  1047. multiple: true,
  1048. 'onUpdate:modelValue': setValue.bind(this),
  1049. },
  1050. [
  1051. h('option', { value: fooValue }),
  1052. h('option', { value: barValue }),
  1053. ],
  1054. ),
  1055. this.value,
  1056. ),
  1057. ]
  1058. },
  1059. })
  1060. render(h(component), root)
  1061. await nextTick()
  1062. const input = root.querySelector('select')
  1063. const [foo, bar] = root.querySelectorAll('option')
  1064. const data = root._vnode.component.data
  1065. foo.selected = true
  1066. triggerEvent('change', input)
  1067. await nextTick()
  1068. expect(data.value).toMatchObject([fooValue])
  1069. foo.selected = false
  1070. bar.selected = true
  1071. triggerEvent('change', input)
  1072. await nextTick()
  1073. expect(data.value).toMatchObject([barValue])
  1074. foo.selected = true
  1075. bar.selected = true
  1076. triggerEvent('change', input)
  1077. await nextTick()
  1078. expect(data.value).toMatchObject([fooValue, barValue])
  1079. // reset
  1080. foo.selected = false
  1081. bar.selected = false
  1082. triggerEvent('change', input)
  1083. await nextTick()
  1084. expect(data.value).toMatchObject([])
  1085. data.value = [fooValue, barValue]
  1086. await nextTick()
  1087. expect(foo.selected).toEqual(true)
  1088. expect(bar.selected).toEqual(true)
  1089. // reset
  1090. foo.selected = false
  1091. bar.selected = false
  1092. triggerEvent('change', input)
  1093. await nextTick()
  1094. expect(data.value).toMatchObject([])
  1095. data.value = [{ foo: 1 }, { bar: 1 }]
  1096. await nextTick()
  1097. // looseEqual
  1098. expect(foo.selected).toEqual(true)
  1099. expect(bar.selected).toEqual(true)
  1100. })
  1101. it('multiple select (model is Set)', async () => {
  1102. const component = defineComponent({
  1103. data() {
  1104. return { value: new Set() }
  1105. },
  1106. render() {
  1107. return [
  1108. withVModel(
  1109. h(
  1110. 'select',
  1111. {
  1112. value: null,
  1113. multiple: true,
  1114. 'onUpdate:modelValue': setValue.bind(this),
  1115. },
  1116. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  1117. ),
  1118. this.value,
  1119. ),
  1120. ]
  1121. },
  1122. })
  1123. render(h(component), root)
  1124. const input = root.querySelector('select')
  1125. const foo = root.querySelector('option[value=foo]')
  1126. const bar = root.querySelector('option[value=bar]')
  1127. const data = root._vnode.component.data
  1128. foo.selected = true
  1129. triggerEvent('change', input)
  1130. await nextTick()
  1131. expect(data.value).toBeInstanceOf(Set)
  1132. expect(data.value).toMatchObject(new Set(['foo']))
  1133. foo.selected = false
  1134. bar.selected = true
  1135. triggerEvent('change', input)
  1136. await nextTick()
  1137. expect(data.value).toBeInstanceOf(Set)
  1138. expect(data.value).toMatchObject(new Set(['bar']))
  1139. foo.selected = true
  1140. bar.selected = true
  1141. triggerEvent('change', input)
  1142. await nextTick()
  1143. expect(data.value).toBeInstanceOf(Set)
  1144. expect(data.value).toMatchObject(new Set(['foo', 'bar']))
  1145. foo.selected = false
  1146. bar.selected = false
  1147. data.value = new Set(['foo'])
  1148. await nextTick()
  1149. expect(input.value).toEqual('foo')
  1150. expect(foo.selected).toEqual(true)
  1151. expect(bar.selected).toEqual(false)
  1152. foo.selected = false
  1153. bar.selected = false
  1154. data.value = new Set(['foo', 'bar'])
  1155. await nextTick()
  1156. expect(foo.selected).toEqual(true)
  1157. expect(bar.selected).toEqual(true)
  1158. })
  1159. it('multiple select (model is Set, option value is object)', async () => {
  1160. const fooValue = { foo: 1 }
  1161. const barValue = { bar: 1 }
  1162. const component = defineComponent({
  1163. data() {
  1164. return { value: new Set() }
  1165. },
  1166. render() {
  1167. return [
  1168. withVModel(
  1169. h(
  1170. 'select',
  1171. {
  1172. value: null,
  1173. multiple: true,
  1174. 'onUpdate:modelValue': setValue.bind(this),
  1175. },
  1176. [
  1177. h('option', { value: fooValue }),
  1178. h('option', { value: barValue }),
  1179. ],
  1180. ),
  1181. this.value,
  1182. ),
  1183. ]
  1184. },
  1185. })
  1186. render(h(component), root)
  1187. await nextTick()
  1188. const input = root.querySelector('select')
  1189. const [foo, bar] = root.querySelectorAll('option')
  1190. const data = root._vnode.component.data
  1191. foo.selected = true
  1192. triggerEvent('change', input)
  1193. await nextTick()
  1194. expect(data.value).toMatchObject(new Set([fooValue]))
  1195. foo.selected = false
  1196. bar.selected = true
  1197. triggerEvent('change', input)
  1198. await nextTick()
  1199. expect(data.value).toMatchObject(new Set([barValue]))
  1200. foo.selected = true
  1201. bar.selected = true
  1202. triggerEvent('change', input)
  1203. await nextTick()
  1204. expect(data.value).toMatchObject(new Set([fooValue, barValue]))
  1205. foo.selected = false
  1206. bar.selected = false
  1207. data.value = new Set([fooValue, barValue])
  1208. await nextTick()
  1209. expect(foo.selected).toEqual(true)
  1210. expect(bar.selected).toEqual(true)
  1211. foo.selected = false
  1212. bar.selected = false
  1213. data.value = new Set([{ foo: 1 }, { bar: 1 }])
  1214. await nextTick()
  1215. // without looseEqual, here is different from Array
  1216. expect(foo.selected).toEqual(false)
  1217. expect(bar.selected).toEqual(false)
  1218. })
  1219. it('should work with composition session', async () => {
  1220. const component = defineComponent({
  1221. data() {
  1222. return { value: '' }
  1223. },
  1224. render() {
  1225. return [
  1226. withVModel(
  1227. h('input', {
  1228. 'onUpdate:modelValue': setValue.bind(this),
  1229. }),
  1230. this.value,
  1231. ),
  1232. ]
  1233. },
  1234. })
  1235. render(h(component), root)
  1236. const input = root.querySelector('input')!
  1237. const data = root._vnode.component.data
  1238. expect(input.value).toEqual('')
  1239. //developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event
  1240. //compositionstart event could be fired after a user starts entering a Chinese character using a Pinyin IME
  1241. input.value = '使用拼音'
  1242. triggerEvent('compositionstart', input)
  1243. await nextTick()
  1244. expect(data.value).toEqual('')
  1245. // input event has no effect during composition session
  1246. input.value = '使用拼音输入'
  1247. triggerEvent('input', input)
  1248. await nextTick()
  1249. expect(data.value).toEqual('')
  1250. // After compositionend event being fired, an input event will be automatically trigger
  1251. triggerEvent('compositionend', input)
  1252. await nextTick()
  1253. expect(data.value).toEqual('使用拼音输入')
  1254. })
  1255. it('multiple select (model is number, option value is string)', async () => {
  1256. const component = defineComponent({
  1257. data() {
  1258. return {
  1259. value: [1, 2],
  1260. }
  1261. },
  1262. render() {
  1263. return [
  1264. withVModel(
  1265. h(
  1266. 'select',
  1267. {
  1268. multiple: true,
  1269. 'onUpdate:modelValue': setValue.bind(this),
  1270. },
  1271. [h('option', { value: '1' }), h('option', { value: '2' })],
  1272. ),
  1273. this.value,
  1274. ),
  1275. ]
  1276. },
  1277. })
  1278. render(h(component), root)
  1279. await nextTick()
  1280. const [foo, bar] = root.querySelectorAll('option')
  1281. expect(foo.selected).toEqual(true)
  1282. expect(bar.selected).toEqual(true)
  1283. })
  1284. // #10503
  1285. test('equal value with a leading 0 should trigger update.', async () => {
  1286. const setNum = function (this: any, value: any) {
  1287. this.num = value
  1288. }
  1289. const component = defineComponent({
  1290. data() {
  1291. return { num: 0 }
  1292. },
  1293. render() {
  1294. return [
  1295. withVModel(
  1296. h('input', {
  1297. id: 'input_num1',
  1298. type: 'number',
  1299. 'onUpdate:modelValue': setNum.bind(this),
  1300. }),
  1301. this.num,
  1302. ),
  1303. ]
  1304. },
  1305. })
  1306. render(h(component), root)
  1307. const data = root._vnode.component.data
  1308. const inputNum1 = root.querySelector('#input_num1')!
  1309. expect(inputNum1.value).toBe('0')
  1310. inputNum1.value = '01'
  1311. triggerEvent('input', inputNum1)
  1312. await nextTick()
  1313. expect(data.num).toBe(1)
  1314. expect(inputNum1.value).toBe('1')
  1315. })
  1316. it(`should support mutating an array or set value for a checkbox`, async () => {
  1317. const component = defineComponent({
  1318. data() {
  1319. return { value: [] }
  1320. },
  1321. render() {
  1322. return [
  1323. withDirectives(
  1324. h('input', {
  1325. type: 'checkbox',
  1326. class: 'foo',
  1327. value: 'foo',
  1328. 'onUpdate:modelValue': setValue.bind(this),
  1329. }),
  1330. [[vModelCheckbox, this.value]],
  1331. ),
  1332. ]
  1333. },
  1334. })
  1335. render(h(component), root)
  1336. const foo = root.querySelector('.foo')
  1337. const data = root._vnode.component.data
  1338. expect(foo.checked).toEqual(false)
  1339. data.value.push('foo')
  1340. await nextTick()
  1341. expect(foo.checked).toEqual(true)
  1342. data.value[0] = 'bar'
  1343. await nextTick()
  1344. expect(foo.checked).toEqual(false)
  1345. data.value = new Set()
  1346. await nextTick()
  1347. expect(foo.checked).toEqual(false)
  1348. data.value.add('foo')
  1349. await nextTick()
  1350. expect(foo.checked).toEqual(true)
  1351. data.value.delete('foo')
  1352. await nextTick()
  1353. expect(foo.checked).toEqual(false)
  1354. })
  1355. })