| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545 |
- import {
- type VNode,
- defineComponent,
- h,
- nextTick,
- ref,
- render,
- vModelCheckbox,
- vModelDynamic,
- withDirectives,
- } from '@vue/runtime-dom'
- const triggerEvent = (type: string, el: Element) => {
- const event = new Event(type)
- el.dispatchEvent(event)
- }
- const withVModel = (node: VNode, arg: any, mods?: any) =>
- withDirectives(node, [[vModelDynamic, arg, '', mods]])
- const setValue = function (this: any, value: any) {
- this.value = value
- }
- let root: any
- beforeEach(() => {
- root = document.createElement('div') as any
- })
- describe('vModel', () => {
- it('should work with text input', async () => {
- const manualListener = vi.fn()
- const component = defineComponent({
- data() {
- return { value: null }
- },
- render() {
- return [
- withVModel(
- h('input', {
- 'onUpdate:modelValue': setValue.bind(this),
- onInput: () => {
- manualListener(data.value)
- },
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('input')!
- const data = root._vnode.component.data
- expect(input.value).toEqual('')
- input.value = 'foo'
- triggerEvent('input', input)
- await nextTick()
- expect(data.value).toEqual('foo')
- // #1931
- expect(manualListener).toHaveBeenCalledWith('foo')
- data.value = 'bar'
- await nextTick()
- expect(input.value).toEqual('bar')
- data.value = undefined
- await nextTick()
- expect(input.value).toEqual('')
- })
- it('should work with number input', async () => {
- const component = defineComponent({
- data() {
- return { value: null }
- },
- render() {
- return [
- withVModel(
- h('input', {
- type: 'number',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('input')!
- const data = root._vnode.component.data
- expect(input.value).toEqual('')
- expect(input.type).toEqual('number')
- input.value = 1
- triggerEvent('input', input)
- await nextTick()
- expect(typeof data.value).toEqual('number')
- expect(data.value).toEqual(1)
- })
- // #7003
- it('should work with number input and be able to update rendering correctly', async () => {
- const setValue1 = function (this: any, value: any) {
- this.value1 = value
- }
- const setValue2 = function (this: any, value: any) {
- this.value2 = value
- }
- const component = defineComponent({
- data() {
- return { value1: 1.002, value2: 1.002 }
- },
- render() {
- return [
- withVModel(
- h('input', {
- id: 'input_num1',
- type: 'number',
- 'onUpdate:modelValue': setValue1.bind(this),
- }),
- this.value1,
- ),
- withVModel(
- h('input', {
- id: 'input_num2',
- type: 'number',
- 'onUpdate:modelValue': setValue2.bind(this),
- }),
- this.value2,
- ),
- ]
- },
- })
- render(h(component), root)
- const data = root._vnode.component.data
- const inputNum1 = root.querySelector('#input_num1')!
- expect(inputNum1.value).toBe('1.002')
- const inputNum2 = root.querySelector('#input_num2')!
- expect(inputNum2.value).toBe('1.002')
- inputNum1.value = '1.00'
- triggerEvent('input', inputNum1)
- await nextTick()
- expect(data.value1).toBe(1)
- inputNum2.value = '1.00'
- triggerEvent('input', inputNum2)
- await nextTick()
- expect(data.value2).toBe(1)
- expect(inputNum1.value).toBe('1.00')
- })
- it('should work with multiple listeners', async () => {
- const spy = vi.fn()
- const component = defineComponent({
- data() {
- return { value: null }
- },
- render() {
- return [
- withVModel(
- h('input', {
- 'onUpdate:modelValue': [setValue.bind(this), spy],
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('input')!
- const data = root._vnode.component.data
- input.value = 'foo'
- triggerEvent('input', input)
- await nextTick()
- expect(data.value).toEqual('foo')
- expect(spy).toHaveBeenCalledWith('foo')
- })
- it('should work with updated listeners', async () => {
- const spy1 = vi.fn()
- const spy2 = vi.fn()
- const toggle = ref(true)
- const component = defineComponent({
- render() {
- return [
- withVModel(
- h('input', {
- 'onUpdate:modelValue': toggle.value ? spy1 : spy2,
- }),
- 'foo',
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('input')!
- input.value = 'foo'
- triggerEvent('input', input)
- await nextTick()
- expect(spy1).toHaveBeenCalledWith('foo')
- // update listener
- toggle.value = false
- await nextTick()
- input.value = 'bar'
- triggerEvent('input', input)
- await nextTick()
- expect(spy1).not.toHaveBeenCalledWith('bar')
- expect(spy2).toHaveBeenCalledWith('bar')
- })
- it('should work with textarea', async () => {
- const component = defineComponent({
- data() {
- return { value: null }
- },
- render() {
- return [
- withVModel(
- h('textarea', {
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('textarea')
- const data = root._vnode.component.data
- input.value = 'foo'
- triggerEvent('input', input)
- await nextTick()
- expect(data.value).toEqual('foo')
- data.value = 'bar'
- await nextTick()
- expect(input.value).toEqual('bar')
- })
- it('should support modifiers', async () => {
- const component = defineComponent({
- data() {
- return {
- number: null,
- trim: null,
- lazy: null,
- trimNumber: null,
- trimLazy: null,
- }
- },
- render() {
- return [
- withVModel(
- h('input', {
- class: 'number',
- 'onUpdate:modelValue': (val: any) => {
- this.number = val
- },
- }),
- this.number,
- {
- number: true,
- },
- ),
- withVModel(
- h('input', {
- class: 'trim',
- 'onUpdate:modelValue': (val: any) => {
- this.trim = val
- },
- }),
- this.trim,
- {
- trim: true,
- },
- ),
- withVModel(
- h('input', {
- class: 'trim-lazy',
- 'onUpdate:modelValue': (val: any) => {
- this.trimLazy = val
- },
- }),
- this.trim,
- {
- trim: true,
- lazy: true,
- },
- ),
- withVModel(
- h('input', {
- class: 'trim-number',
- 'onUpdate:modelValue': (val: any) => {
- this.trimNumber = val
- },
- }),
- this.trimNumber,
- {
- trim: true,
- number: true,
- },
- ),
- withVModel(
- h('input', {
- class: 'lazy',
- 'onUpdate:modelValue': (val: any) => {
- this.lazy = val
- },
- }),
- this.lazy,
- {
- lazy: true,
- },
- ),
- ]
- },
- })
- render(h(component), root)
- const number = root.querySelector('.number')
- const trim = root.querySelector('.trim')
- const trimNumber = root.querySelector('.trim-number')
- const trimLazy = root.querySelector('.trim-lazy')
- const lazy = root.querySelector('.lazy')
- const data = root._vnode.component.data
- number.value = '+01.2'
- triggerEvent('input', number)
- await nextTick()
- expect(data.number).toEqual(1.2)
- triggerEvent('change', number)
- await nextTick()
- expect(number.value).toEqual('1.2')
- trim.value = ' hello, world '
- triggerEvent('input', trim)
- await nextTick()
- expect(data.trim).toEqual('hello, world')
- trimNumber.value = ' 1 '
- triggerEvent('input', trimNumber)
- await nextTick()
- expect(data.trimNumber).toEqual(1)
- trimNumber.value = ' +01.2 '
- triggerEvent('input', trimNumber)
- await nextTick()
- expect(data.trimNumber).toEqual(1.2)
- trimLazy.value = ' ddd '
- triggerEvent('change', trimLazy)
- await nextTick()
- expect(data.trimLazy).toEqual('ddd')
- lazy.value = 'foo'
- triggerEvent('change', lazy)
- await nextTick()
- expect(data.lazy).toEqual('foo')
- })
- it('should preserve unresolved trimmed text while focused in nested shadow roots', async () => {
- const model = ref('')
- const component = defineComponent({
- render() {
- return withVModel(
- h('input', {
- 'onUpdate:modelValue': (value: string) => {
- model.value = value
- },
- }),
- model.value,
- {
- trim: true,
- },
- )
- },
- })
- document.body.appendChild(root)
- const outerShadowRoot = root.attachShadow({ mode: 'open' })
- const innerHost = document.createElement('div')
- outerShadowRoot.appendChild(innerHost)
- const innerShadowRoot = innerHost.attachShadow({ mode: 'open' })
- try {
- render(h(component), innerShadowRoot)
- const input = innerShadowRoot.querySelector('input') as HTMLInputElement
- input.focus()
- expect(document.activeElement).toBe(root)
- expect(outerShadowRoot.activeElement).toBe(innerHost)
- expect(innerShadowRoot.activeElement).toBe(input)
- input.value = ' hello, world '
- triggerEvent('input', input)
- await nextTick()
- expect(model.value).toEqual('hello, world')
- expect(input.value).toEqual(' hello, world ')
- } finally {
- render(null, innerShadowRoot)
- root.remove()
- }
- })
- it('should work with range', async () => {
- const component = defineComponent({
- data() {
- return { value: 25 }
- },
- render() {
- return [
- withVModel(
- h('input', {
- type: 'range',
- min: 1,
- max: 100,
- class: 'foo',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- {
- number: true,
- },
- ),
- withVModel(
- h('input', {
- type: 'range',
- min: 1,
- max: 100,
- class: 'bar',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- {
- lazy: true,
- },
- ),
- ]
- },
- })
- render(h(component), root)
- const foo = root.querySelector('.foo')
- const bar = root.querySelector('.bar')
- const data = root._vnode.component.data
- foo.value = 20
- triggerEvent('input', foo)
- await nextTick()
- expect(data.value).toEqual(20)
- foo.value = 200
- triggerEvent('input', foo)
- await nextTick()
- expect(data.value).toEqual(100)
- foo.value = -1
- triggerEvent('input', foo)
- await nextTick()
- expect(data.value).toEqual(1)
- bar.value = 30
- triggerEvent('change', bar)
- await nextTick()
- expect(data.value).toEqual('30')
- bar.value = 200
- triggerEvent('change', bar)
- await nextTick()
- expect(data.value).toEqual('100')
- bar.value = -1
- triggerEvent('change', bar)
- await nextTick()
- expect(data.value).toEqual('1')
- data.value = 60
- await nextTick()
- expect(foo.value).toEqual('60')
- expect(bar.value).toEqual('60')
- data.value = -1
- await nextTick()
- expect(foo.value).toEqual('1')
- expect(bar.value).toEqual('1')
- data.value = 200
- await nextTick()
- expect(foo.value).toEqual('100')
- expect(bar.value).toEqual('100')
- })
- it('should work with checkbox', async () => {
- const component = defineComponent({
- data() {
- return { value: null }
- },
- render() {
- return [
- withVModel(
- h('input', {
- type: 'checkbox',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('input')
- const data = root._vnode.component.data
- input.checked = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toEqual(true)
- data.value = false
- await nextTick()
- expect(input.checked).toEqual(false)
- data.value = true
- await nextTick()
- expect(input.checked).toEqual(true)
- input.checked = false
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toEqual(false)
- })
- it('should work with checkbox and true-value/false-value', async () => {
- const component = defineComponent({
- data() {
- return { value: 'yes' }
- },
- render() {
- return [
- withVModel(
- h('input', {
- type: 'checkbox',
- 'true-value': 'yes',
- 'false-value': 'no',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('input')
- const data = root._vnode.component.data
- // DOM checked state should respect initial true-value/false-value
- expect(input.checked).toEqual(true)
- input.checked = false
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toEqual('no')
- data.value = 'yes'
- await nextTick()
- expect(input.checked).toEqual(true)
- data.value = 'no'
- await nextTick()
- expect(input.checked).toEqual(false)
- input.checked = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toEqual('yes')
- })
- it('should work with checkbox and true-value/false-value with object values', async () => {
- const component = defineComponent({
- data() {
- return { value: null }
- },
- render() {
- return [
- withVModel(
- h('input', {
- type: 'checkbox',
- 'true-value': { yes: 'yes' },
- 'false-value': { no: 'no' },
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('input')
- const data = root._vnode.component.data
- input.checked = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toEqual({ yes: 'yes' })
- data.value = { no: 'no' }
- await nextTick()
- expect(input.checked).toEqual(false)
- data.value = { yes: 'yes' }
- await nextTick()
- expect(input.checked).toEqual(true)
- input.checked = false
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toEqual({ no: 'no' })
- })
- it(`should support array as a checkbox model`, async () => {
- const component = defineComponent({
- data() {
- return { value: [] }
- },
- render() {
- return [
- withVModel(
- h('input', {
- type: 'checkbox',
- class: 'foo',
- value: 'foo',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- withVModel(
- h('input', {
- type: 'checkbox',
- class: 'bar',
- value: 'bar',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const foo = root.querySelector('.foo')
- const bar = root.querySelector('.bar')
- const data = root._vnode.component.data
- foo.checked = true
- triggerEvent('change', foo)
- await nextTick()
- expect(data.value).toMatchObject(['foo'])
- bar.checked = true
- triggerEvent('change', bar)
- await nextTick()
- expect(data.value).toMatchObject(['foo', 'bar'])
- bar.checked = false
- triggerEvent('change', bar)
- await nextTick()
- expect(data.value).toMatchObject(['foo'])
- foo.checked = false
- triggerEvent('change', foo)
- await nextTick()
- expect(data.value).toMatchObject([])
- data.value = ['foo']
- await nextTick()
- expect(bar.checked).toEqual(false)
- expect(foo.checked).toEqual(true)
- data.value = ['bar']
- await nextTick()
- expect(foo.checked).toEqual(false)
- expect(bar.checked).toEqual(true)
- data.value = []
- await nextTick()
- expect(foo.checked).toEqual(false)
- expect(bar.checked).toEqual(false)
- })
- it(`should support Set as a checkbox model`, async () => {
- const component = defineComponent({
- data() {
- return { value: new Set() }
- },
- render() {
- return [
- withVModel(
- h('input', {
- type: 'checkbox',
- class: 'foo',
- value: 'foo',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- withVModel(
- h('input', {
- type: 'checkbox',
- class: 'bar',
- value: 'bar',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const foo = root.querySelector('.foo')
- const bar = root.querySelector('.bar')
- const data = root._vnode.component.data
- foo.checked = true
- triggerEvent('change', foo)
- await nextTick()
- expect(data.value).toMatchObject(new Set(['foo']))
- bar.checked = true
- triggerEvent('change', bar)
- await nextTick()
- expect(data.value).toMatchObject(new Set(['foo', 'bar']))
- bar.checked = false
- triggerEvent('change', bar)
- await nextTick()
- expect(data.value).toMatchObject(new Set(['foo']))
- foo.checked = false
- triggerEvent('change', foo)
- await nextTick()
- expect(data.value).toMatchObject(new Set())
- data.value = new Set(['foo'])
- await nextTick()
- expect(bar.checked).toEqual(false)
- expect(foo.checked).toEqual(true)
- data.value = new Set(['bar'])
- await nextTick()
- expect(foo.checked).toEqual(false)
- expect(bar.checked).toEqual(true)
- data.value = new Set()
- await nextTick()
- expect(foo.checked).toEqual(false)
- expect(bar.checked).toEqual(false)
- })
- it('should not update DOM unnecessarily', async () => {
- const component = defineComponent({
- data() {
- return { value: true }
- },
- render() {
- return [
- withVModel(
- h('input', {
- type: 'checkbox',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('input')
- const data = root._vnode.component.data
- const setCheckedSpy = vi.spyOn(input, 'checked', 'set')
- // Trigger a change event without actually changing the value
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toEqual(true)
- expect(setCheckedSpy).not.toHaveBeenCalled()
- // Change the value and trigger a change event
- input.checked = false
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toEqual(false)
- expect(setCheckedSpy).toHaveBeenCalledTimes(1)
- setCheckedSpy.mockClear()
- data.value = false
- await nextTick()
- expect(input.checked).toEqual(false)
- expect(setCheckedSpy).not.toHaveBeenCalled()
- data.value = true
- await nextTick()
- expect(input.checked).toEqual(true)
- expect(setCheckedSpy).toHaveBeenCalledTimes(1)
- })
- it('should handle array values correctly without unnecessary updates', async () => {
- const component = defineComponent({
- data() {
- return { value: ['foo'] }
- },
- render() {
- return [
- withVModel(
- h('input', {
- type: 'checkbox',
- value: 'foo',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- withVModel(
- h('input', {
- type: 'checkbox',
- value: 'bar',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const [foo, bar] = root.querySelectorAll('input')
- const data = root._vnode.component.data
- const setCheckedSpyFoo = vi.spyOn(foo, 'checked', 'set')
- const setCheckedSpyBar = vi.spyOn(bar, 'checked', 'set')
- expect(foo.checked).toEqual(true)
- expect(bar.checked).toEqual(false)
- triggerEvent('change', foo)
- await nextTick()
- expect(data.value).toEqual(['foo'])
- expect(setCheckedSpyFoo).not.toHaveBeenCalled()
- bar.checked = true
- triggerEvent('change', bar)
- await nextTick()
- expect(data.value).toEqual(['foo', 'bar'])
- expect(setCheckedSpyBar).toHaveBeenCalledTimes(1)
- setCheckedSpyFoo.mockClear()
- setCheckedSpyBar.mockClear()
- data.value = ['foo', 'bar']
- await nextTick()
- expect(setCheckedSpyFoo).not.toHaveBeenCalled()
- expect(setCheckedSpyBar).not.toHaveBeenCalled()
- data.value = ['bar']
- await nextTick()
- expect(setCheckedSpyFoo).toHaveBeenCalledTimes(1)
- expect(setCheckedSpyBar).not.toHaveBeenCalled()
- expect(foo.checked).toEqual(false)
- expect(bar.checked).toEqual(true)
- })
- it('should work with radio', async () => {
- const component = defineComponent({
- data() {
- return { value: null }
- },
- render() {
- return [
- withVModel(
- h('input', {
- type: 'radio',
- class: 'foo',
- value: 'foo',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- withVModel(
- h('input', {
- type: 'radio',
- class: 'bar',
- value: 'bar',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const foo = root.querySelector('.foo')
- const bar = root.querySelector('.bar')
- const data = root._vnode.component.data
- foo.checked = true
- triggerEvent('change', foo)
- await nextTick()
- expect(data.value).toEqual('foo')
- bar.checked = true
- triggerEvent('change', bar)
- await nextTick()
- expect(data.value).toEqual('bar')
- data.value = null
- await nextTick()
- expect(foo.checked).toEqual(false)
- expect(bar.checked).toEqual(false)
- data.value = 'foo'
- await nextTick()
- expect(foo.checked).toEqual(true)
- expect(bar.checked).toEqual(false)
- data.value = 'bar'
- await nextTick()
- expect(foo.checked).toEqual(false)
- expect(bar.checked).toEqual(true)
- })
- it('should work with single select', async () => {
- const component = defineComponent({
- data() {
- return { value: null }
- },
- render() {
- return [
- withVModel(
- h(
- 'select',
- {
- value: null,
- 'onUpdate:modelValue': setValue.bind(this),
- },
- [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
- ),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('select')
- const foo = root.querySelector('option[value=foo]')
- const bar = root.querySelector('option[value=bar]')
- const data = root._vnode.component.data
- foo.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toEqual('foo')
- foo.selected = false
- bar.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toEqual('bar')
- foo.selected = false
- bar.selected = false
- data.value = 'foo'
- await nextTick()
- expect(input.value).toEqual('foo')
- expect(foo.selected).toEqual(true)
- expect(bar.selected).toEqual(false)
- foo.selected = true
- bar.selected = false
- data.value = 'bar'
- await nextTick()
- expect(input.value).toEqual('bar')
- expect(foo.selected).toEqual(false)
- expect(bar.selected).toEqual(true)
- })
- it('multiple select (model is Array)', async () => {
- const component = defineComponent({
- data() {
- return { value: [] }
- },
- render() {
- return [
- withVModel(
- h(
- 'select',
- {
- value: null,
- multiple: true,
- 'onUpdate:modelValue': setValue.bind(this),
- },
- [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
- ),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('select')
- const foo = root.querySelector('option[value=foo]')
- const bar = root.querySelector('option[value=bar]')
- const data = root._vnode.component.data
- foo.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject(['foo'])
- foo.selected = false
- bar.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject(['bar'])
- foo.selected = true
- bar.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject(['foo', 'bar'])
- foo.selected = false
- bar.selected = false
- data.value = ['foo']
- await nextTick()
- expect(input.value).toEqual('foo')
- expect(foo.selected).toEqual(true)
- expect(bar.selected).toEqual(false)
- foo.selected = false
- bar.selected = false
- data.value = ['foo', 'bar']
- await nextTick()
- expect(foo.selected).toEqual(true)
- expect(bar.selected).toEqual(true)
- })
- it('v-model.number should work with select tag', async () => {
- const component = defineComponent({
- data() {
- return { value: null }
- },
- render() {
- return [
- withVModel(
- h(
- 'select',
- {
- value: null,
- 'onUpdate:modelValue': setValue.bind(this),
- },
- [h('option', { value: '1' }), h('option', { value: '2' })],
- ),
- this.value,
- {
- number: true,
- },
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('select')
- const one = root.querySelector('option[value="1"]')
- const data = root._vnode.component.data
- one.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(typeof data.value).toEqual('number')
- expect(data.value).toEqual(1)
- })
- it('v-model.number should work with multiple select', async () => {
- const component = defineComponent({
- data() {
- return { value: [] }
- },
- render() {
- return [
- withVModel(
- h(
- 'select',
- {
- value: null,
- multiple: true,
- 'onUpdate:modelValue': setValue.bind(this),
- },
- [h('option', { value: '1' }), h('option', { value: '2' })],
- ),
- this.value,
- {
- number: true,
- },
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('select')
- const one = root.querySelector('option[value="1"]')
- const two = root.querySelector('option[value="2"]')
- const data = root._vnode.component.data
- one.selected = true
- two.selected = false
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject([1])
- one.selected = false
- two.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject([2])
- one.selected = true
- two.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject([1, 2])
- one.selected = false
- two.selected = false
- data.value = [1]
- await nextTick()
- expect(one.selected).toEqual(true)
- expect(two.selected).toEqual(false)
- one.selected = false
- two.selected = false
- data.value = [1, 2]
- await nextTick()
- expect(one.selected).toEqual(true)
- expect(two.selected).toEqual(true)
- })
- it('multiple select (model is Array, option value is object)', async () => {
- const fooValue = { foo: 1 }
- const barValue = { bar: 1 }
- const component = defineComponent({
- data() {
- return { value: [] }
- },
- render() {
- return [
- withVModel(
- h(
- 'select',
- {
- value: null,
- multiple: true,
- 'onUpdate:modelValue': setValue.bind(this),
- },
- [
- h('option', { value: fooValue }),
- h('option', { value: barValue }),
- ],
- ),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- await nextTick()
- const input = root.querySelector('select')
- const [foo, bar] = root.querySelectorAll('option')
- const data = root._vnode.component.data
- foo.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject([fooValue])
- foo.selected = false
- bar.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject([barValue])
- foo.selected = true
- bar.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject([fooValue, barValue])
- // reset
- foo.selected = false
- bar.selected = false
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject([])
- data.value = [fooValue, barValue]
- await nextTick()
- expect(foo.selected).toEqual(true)
- expect(bar.selected).toEqual(true)
- // reset
- foo.selected = false
- bar.selected = false
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject([])
- data.value = [{ foo: 1 }, { bar: 1 }]
- await nextTick()
- // looseEqual
- expect(foo.selected).toEqual(true)
- expect(bar.selected).toEqual(true)
- })
- it('multiple select (model is Set)', async () => {
- const component = defineComponent({
- data() {
- return { value: new Set() }
- },
- render() {
- return [
- withVModel(
- h(
- 'select',
- {
- value: null,
- multiple: true,
- 'onUpdate:modelValue': setValue.bind(this),
- },
- [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
- ),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('select')
- const foo = root.querySelector('option[value=foo]')
- const bar = root.querySelector('option[value=bar]')
- const data = root._vnode.component.data
- foo.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toBeInstanceOf(Set)
- expect(data.value).toMatchObject(new Set(['foo']))
- foo.selected = false
- bar.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toBeInstanceOf(Set)
- expect(data.value).toMatchObject(new Set(['bar']))
- foo.selected = true
- bar.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toBeInstanceOf(Set)
- expect(data.value).toMatchObject(new Set(['foo', 'bar']))
- foo.selected = false
- bar.selected = false
- data.value = new Set(['foo'])
- await nextTick()
- expect(input.value).toEqual('foo')
- expect(foo.selected).toEqual(true)
- expect(bar.selected).toEqual(false)
- foo.selected = false
- bar.selected = false
- data.value = new Set(['foo', 'bar'])
- await nextTick()
- expect(foo.selected).toEqual(true)
- expect(bar.selected).toEqual(true)
- })
- it('multiple select (model is Set, option value is object)', async () => {
- const fooValue = { foo: 1 }
- const barValue = { bar: 1 }
- const component = defineComponent({
- data() {
- return { value: new Set() }
- },
- render() {
- return [
- withVModel(
- h(
- 'select',
- {
- value: null,
- multiple: true,
- 'onUpdate:modelValue': setValue.bind(this),
- },
- [
- h('option', { value: fooValue }),
- h('option', { value: barValue }),
- ],
- ),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- await nextTick()
- const input = root.querySelector('select')
- const [foo, bar] = root.querySelectorAll('option')
- const data = root._vnode.component.data
- foo.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject(new Set([fooValue]))
- foo.selected = false
- bar.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject(new Set([barValue]))
- foo.selected = true
- bar.selected = true
- triggerEvent('change', input)
- await nextTick()
- expect(data.value).toMatchObject(new Set([fooValue, barValue]))
- foo.selected = false
- bar.selected = false
- data.value = new Set([fooValue, barValue])
- await nextTick()
- expect(foo.selected).toEqual(true)
- expect(bar.selected).toEqual(true)
- foo.selected = false
- bar.selected = false
- data.value = new Set([{ foo: 1 }, { bar: 1 }])
- await nextTick()
- // without looseEqual, here is different from Array
- expect(foo.selected).toEqual(false)
- expect(bar.selected).toEqual(false)
- })
- it('should work with composition session', async () => {
- const component = defineComponent({
- data() {
- return { value: '' }
- },
- render() {
- return [
- withVModel(
- h('input', {
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- const input = root.querySelector('input')!
- const data = root._vnode.component.data
- expect(input.value).toEqual('')
- //developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event
- //compositionstart event could be fired after a user starts entering a Chinese character using a Pinyin IME
- input.value = '使用拼音'
- triggerEvent('compositionstart', input)
- await nextTick()
- expect(data.value).toEqual('')
- // input event has no effect during composition session
- input.value = '使用拼音输入'
- triggerEvent('input', input)
- await nextTick()
- expect(data.value).toEqual('')
- // After compositionend event being fired, an input event will be automatically trigger
- triggerEvent('compositionend', input)
- await nextTick()
- expect(data.value).toEqual('使用拼音输入')
- })
- it('multiple select (model is number, option value is string)', async () => {
- const component = defineComponent({
- data() {
- return {
- value: [1, 2],
- }
- },
- render() {
- return [
- withVModel(
- h(
- 'select',
- {
- multiple: true,
- 'onUpdate:modelValue': setValue.bind(this),
- },
- [h('option', { value: '1' }), h('option', { value: '2' })],
- ),
- this.value,
- ),
- ]
- },
- })
- render(h(component), root)
- await nextTick()
- const [foo, bar] = root.querySelectorAll('option')
- expect(foo.selected).toEqual(true)
- expect(bar.selected).toEqual(true)
- })
- // #10503
- test('equal value with a leading 0 should trigger update.', async () => {
- const setNum = function (this: any, value: any) {
- this.num = value
- }
- const component = defineComponent({
- data() {
- return { num: 0 }
- },
- render() {
- return [
- withVModel(
- h('input', {
- id: 'input_num1',
- type: 'number',
- 'onUpdate:modelValue': setNum.bind(this),
- }),
- this.num,
- ),
- ]
- },
- })
- render(h(component), root)
- const data = root._vnode.component.data
- const inputNum1 = root.querySelector('#input_num1')!
- expect(inputNum1.value).toBe('0')
- inputNum1.value = '01'
- triggerEvent('input', inputNum1)
- await nextTick()
- expect(data.num).toBe(1)
- expect(inputNum1.value).toBe('1')
- })
- it(`should support mutating an array or set value for a checkbox`, async () => {
- const component = defineComponent({
- data() {
- return { value: [] }
- },
- render() {
- return [
- withDirectives(
- h('input', {
- type: 'checkbox',
- class: 'foo',
- value: 'foo',
- 'onUpdate:modelValue': setValue.bind(this),
- }),
- [[vModelCheckbox, this.value]],
- ),
- ]
- },
- })
- render(h(component), root)
- const foo = root.querySelector('.foo')
- const data = root._vnode.component.data
- expect(foo.checked).toEqual(false)
- data.value.push('foo')
- await nextTick()
- expect(foo.checked).toEqual(true)
- data.value[0] = 'bar'
- await nextTick()
- expect(foo.checked).toEqual(false)
- data.value = new Set()
- await nextTick()
- expect(foo.checked).toEqual(false)
- data.value.add('foo')
- await nextTick()
- expect(foo.checked).toEqual(true)
- data.value.delete('foo')
- await nextTick()
- expect(foo.checked).toEqual(false)
- })
- })
|