test(runtime-dom): vModel tests (#255)
This commit is contained in:
parent
66023a8886
commit
b4eeb2fa1b
441
packages/runtime-dom/__tests__/vModel.spec.ts
Normal file
441
packages/runtime-dom/__tests__/vModel.spec.ts
Normal file
@ -0,0 +1,441 @@
|
||||
import {
|
||||
createApp,
|
||||
h,
|
||||
nextTick,
|
||||
createComponent,
|
||||
vModelDynamic,
|
||||
applyDirectives,
|
||||
VNode
|
||||
} 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) =>
|
||||
applyDirectives(node, [[vModelDynamic, arg, '', mods]])
|
||||
|
||||
const setValue = function(this: any, value: any) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
let app: any, root: any
|
||||
|
||||
beforeEach(() => {
|
||||
app = createApp()
|
||||
root = document.createElement('div') as any
|
||||
})
|
||||
|
||||
describe('vModel', () => {
|
||||
it('should work with text input', async () => {
|
||||
const component = createComponent({
|
||||
data() {
|
||||
return { value: null }
|
||||
},
|
||||
render() {
|
||||
return [
|
||||
withVModel(
|
||||
h('input', {
|
||||
'onUpdate:modelValue': setValue.bind(this)
|
||||
}),
|
||||
this.value
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
app.mount(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')
|
||||
|
||||
data.value = 'bar'
|
||||
await nextTick()
|
||||
expect(input.value).toEqual('bar')
|
||||
})
|
||||
|
||||
it('should work with textarea', async () => {
|
||||
const component = createComponent({
|
||||
data() {
|
||||
return { value: null }
|
||||
},
|
||||
render() {
|
||||
return [
|
||||
withVModel(
|
||||
h('textarea', {
|
||||
'onUpdate:modelValue': setValue.bind(this)
|
||||
}),
|
||||
this.value
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
app.mount(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 = createComponent({
|
||||
data() {
|
||||
return { number: null, trim: null, lazy: 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: 'lazy',
|
||||
'onUpdate:modelValue': (val: any) => {
|
||||
this.lazy = val
|
||||
}
|
||||
}),
|
||||
this.lazy,
|
||||
{
|
||||
lazy: true
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
app.mount(component, root)
|
||||
|
||||
const number = root.querySelector('.number')
|
||||
const trim = root.querySelector('.trim')
|
||||
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)
|
||||
|
||||
trim.value = ' hello, world '
|
||||
triggerEvent('input', trim)
|
||||
await nextTick()
|
||||
expect(data.trim).toEqual('hello, world')
|
||||
|
||||
lazy.value = 'foo'
|
||||
triggerEvent('change', lazy)
|
||||
await nextTick()
|
||||
expect(data.lazy).toEqual('foo')
|
||||
})
|
||||
|
||||
it('should work with checkbox', async () => {
|
||||
const component = createComponent({
|
||||
data() {
|
||||
return { value: null }
|
||||
},
|
||||
render() {
|
||||
return [
|
||||
withVModel(
|
||||
h('input', {
|
||||
type: 'checkbox',
|
||||
'onUpdate:modelValue': setValue.bind(this)
|
||||
}),
|
||||
this.value
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
app.mount(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)
|
||||
})
|
||||
|
||||
it(`should support array as a checkbox model`, async () => {
|
||||
const component = createComponent({
|
||||
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
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
app.mount(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 work with radio', async () => {
|
||||
const component = createComponent({
|
||||
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
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
app.mount(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 = createComponent({
|
||||
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
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
app.mount(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('should work with multiple select', async () => {
|
||||
const component = createComponent({
|
||||
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
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
app.mount(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)
|
||||
})
|
||||
})
|
@ -79,15 +79,19 @@ export const vModelCheckbox: Directive<HTMLInputElement> = {
|
||||
addEventListener(el, 'change', () => {
|
||||
const modelValue = (el as any)._modelValue
|
||||
const elementValue = getValue(el)
|
||||
const checked = el.checked
|
||||
if (isArray(modelValue)) {
|
||||
const i = looseIndexOf(modelValue, elementValue)
|
||||
if (i > -1) {
|
||||
assign([...modelValue.slice(0, i), ...modelValue.slice(i + 1)])
|
||||
} else {
|
||||
const index = looseIndexOf(modelValue, elementValue)
|
||||
const found = index !== -1
|
||||
if (checked && !found) {
|
||||
assign(modelValue.concat(elementValue))
|
||||
} else if (!checked && found) {
|
||||
const filtered = [...modelValue]
|
||||
filtered.splice(index, 1)
|
||||
assign(filtered)
|
||||
}
|
||||
} else {
|
||||
assign(el.checked)
|
||||
assign(checked)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user