import Vue from 'vue'
import { SSR_ATTR } from 'shared/constants'
describe('vdom patch: edge cases', () => {
it('should handle static vnodes by key', done => {
const vm = new Vue({
data: {
ok: true
},
template: `
<div>
<div v-for="i in 2">
<div v-if="ok">a</div><div>b</div><div v-if="!ok">c</div><div>d</div>
</div>
</div>
`
}).$mount()
expect(vm.$el.textContent).toBe('abdabd')
vm.ok = false
waitForUpdate(() => {
expect(vm.$el.textContent).toBe('bcdbcd')
}).then(done)
})
it("should return listener's result for method name and function expression with and w/o modifiers", done => {
const dummyEvt = { preventDefault: () => {} }
new Vue({
template: `
<div v-test>
<div @click="addFive"></div>
<div @click.prevent="addFive"></div>
<div @click="addFive($event, 5)"></div>
<div @click.prevent="addFive($event, 5)"></div>
</div>
`,
methods: {
addFive($event, toAdd = 0) {
return toAdd + 5
}
},
directives: {
test: {
bind(el, binding, vnode) {
waitForUpdate(() => {
expect(vnode.children[0].data.on.click()).toBe(5)
expect(vnode.children[2].data.on.click(dummyEvt)).toBe(5)
expect(vnode.children[4].data.on.click()).toBe(10)
expect(vnode.children[6].data.on.click(dummyEvt)).toBe(10)
}).then(done)
}
}
}
}).$mount()
})
it("should handle static node edge case when it's reused AND used as a reference node for insertion", done => {
const vm = new Vue({
data: {
ok: true
},
template: `
<div>
<button @click="ok = !ok">toggle</button>
<div class="b" v-if="ok">123</div>
<div class="c">
<div><span/></div><p>{{ 1 }}</p>
</div>
<div class="d">
<label>{{ 2 }}</label>
</div>
<div class="b" v-if="ok">123</div>
</div>
`
}).$mount()
expect(vm.$el.querySelector('.c').textContent).toBe('1')
expect(vm.$el.querySelector('.d').textContent).toBe('2')
vm.ok = false
waitForUpdate(() => {
expect(vm.$el.querySelector('.c').textContent).toBe('1')
expect(vm.$el.querySelector('.d').textContent).toBe('2')
}).then(done)
})
it('should handle slot nodes being reused across render', done => {
const vm = new Vue({
template: `
<foo ref="foo">
<div>slot</div>
</foo>
`,
components: {
foo: {
data() {
return { ok: true }
},
render(h) {
const children = [
this.ok ? h('div', 'toggler ') : null,
h('div', [this.$slots.default, h('span', ' 1')]),
h('div', [h('label', ' 2')])
]
return h('div', children)
}
}
}
}).$mount()
expect(vm.$el.textContent).toContain('toggler slot 1 2')
vm.$refs.foo.ok = false
waitForUpdate(() => {
expect(vm.$el.textContent).toContain('slot 1 2')
vm.$refs.foo.ok = true
})
.then(() => {
expect(vm.$el.textContent).toContain('toggler slot 1 2')
vm.$refs.foo.ok = false
})
.then(() => {
expect(vm.$el.textContent).toContain('slot 1 2')
vm.$refs.foo.ok = true
})
.then(done)
})
it("should synchronize vm' vnode", done => {
const comp = {
data: () => ({ swap: true }),
render(h) {
return this.swap ? h('a', 'atag') : h('span', 'span')
}
}
const wrapper = {
render: h => h('comp'),
components: { comp }
}
const vm = new Vue({
render(h) {
const children = [h('wrapper'), h('div', 'row')]
if (this.swap) {
children.reverse()
}
return h('div', children)
},
data: () => ({ swap: false }),
components: { wrapper }
}).$mount()
expect(vm.$el.innerHTML).toBe('<a>atag</a><div>row</div>')
const wrapperVm = vm.$children[0]
const compVm = wrapperVm.$children[0]
vm.swap = true
waitForUpdate(() => {
expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
expect(vm.$el.innerHTML).toBe('<div>row</div><a>atag</a>')
vm.swap = false
})
.then(() => {
expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
expect(vm.$el.innerHTML).toBe('<a>atag</a><div>row</div>')
compVm.swap = false
})
.then(() => {
expect(vm.$el.innerHTML).toBe('<span>span</span><div>row</div>')
expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
vm.swap = true
})
.then(() => {
expect(vm.$el.innerHTML).toBe('<div>row</div><span>span</span>')
expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
vm.swap = true
})
.then(done)
})
it('should not reset value when patching between dynamic/static bindings', done => {
const vm = new Vue({
data: { ok: true },
template: `
<div>
<input type="button" v-if="ok" value="a">
<input type="button" :value="'b'">
</div>
`
}).$mount()
expect(vm.$el.children[0].value).toBe('a')
vm.ok = false
waitForUpdate(() => {
expect(vm.$el.children[0].value).toBe('b')
vm.ok = true
})
.then(() => {
expect(vm.$el.children[0].value).toBe('a')
})
.then(done)
})
it('should not replace node when switching between text-like inputs', done => {
const vm = new Vue({
data: { show: false },
template: `
<div>
<input :type="show ? 'text' : 'password'">
</div>
`
}).$mount()
const node = vm.$el.children[0]
expect(vm.$el.children[0].type).toBe('password')
vm.$el.children[0].value = 'test'
vm.show = true
waitForUpdate(() => {
expect(vm.$el.children[0]).toBe(node)
expect(vm.$el.children[0].value).toBe('test')
expect(vm.$el.children[0].type).toBe('text')
vm.show = false
})
.then(() => {
expect(vm.$el.children[0]).toBe(node)
expect(vm.$el.children[0].value).toBe('test')
expect(vm.$el.children[0].type).toBe('password')
})
.then(done)
})
it('should properly patch nested HOC when root element is replaced', done => {
const vm = new Vue({
template: `<foo class="hello" ref="foo" />`,
components: {
foo: {
template: `<bar ref="bar" />`,
components: {
bar: {
template: `<div v-if="ok"></div><span v-else></span>`,
data() {
return { ok: true }
}
}
}
}
}
}).$mount()
expect(vm.$refs.foo.$refs.bar.$el.tagName).toBe('DIV')
expect(vm.$refs.foo.$refs.bar.$el.className).toBe(`hello`)
expect(vm.$el.tagName).toBe('DIV')
expect(vm.$el.className).toBe(`hello`)
vm.$refs.foo.$refs.bar.ok = false
waitForUpdate(() => {
expect(vm.$refs.foo.$refs.bar.$el.tagName).toBe('SPAN')
expect(vm.$refs.foo.$refs.bar.$el.className).toBe(`hello`)
expect(vm.$el.tagName).toBe('SPAN')
expect(vm.$el.className).toBe(`hello`)
}).then(done)
})
it('should not render undefined for empty nested arrays', () => {
const vm = new Vue({
template: `<div><template v-for="i in emptyArr"></template></div>`,
data: { emptyArr: [] }
}).$mount()
expect(vm.$el.textContent).toBe('')
})
it('backwards compat with checkbox code generated before 2.4', () => {
const spy = vi.fn()
const vm = new Vue({
data: {
label: 'foobar',
name: 'foobar'
},
computed: {
value: {
get() {
return 1
},
set: spy
}
},
render(h) {
const _vm = this
return h('div', {}, [
h('input', {
directives: [
{
name: 'model',
rawName: 'v-model',
value: _vm.value,
expression: 'value'
}
],
attrs: {
type: 'radio',
name: _vm.name
},
domProps: {
value: _vm.label,
checked: _vm._q(_vm.value, _vm.label)
},
on: {
__c: function ($event) {
_vm.value = _vm.label
}
}
})
])
}
}).$mount()
document.body.appendChild(vm.$el)
vm.$el.children[0].click()
expect(spy).toHaveBeenCalled()
})
it('transition children with only deep bindings should be patched on update', done => {
const vm = new Vue({
template: `
<div>
<transition>
<div :style="style"></div>
</transition>
</div>
`,
data: () => ({
style: { color: 'red' }
})
}).$mount()
expect(vm.$el.children[0].style.color).toBe('red')
vm.style.color = 'green'
waitForUpdate(() => {
expect(vm.$el.children[0].style.color).toBe('green')
}).then(done)
})
it('should cleanup component inline events on patch when no events are present', done => {
const log = vi.fn()
const vm = new Vue({
data: { ok: true },
template: `
<div>
<foo v-if="ok" @custom="log"/>
<foo v-else/>
</div>
`,
components: {
foo: {
render() {}
}
},
methods: { log }
}).$mount()
vm.ok = false
waitForUpdate(() => {
vm.$children[0].$emit('custom')
expect(log).not.toHaveBeenCalled()
}).then(done)
})
it('should not special-case boolean attributes for custom elements', () => {
Vue.config.ignoredElements = [/^custom-/]
const vm = new Vue({
template: `<div><custom-foo selected="1"/></div>`
}).$mount()
expect(vm.$el.querySelector('custom-foo').getAttribute('selected')).toBe(
'1'
)
Vue.config.ignoredElements = []
})
it('should not cause duplicate init when components share data object', () => {
const Base = {
render(h) {
return h('div', this.$options.name)
}
}
const Foo = {
name: 'Foo',
extends: Base
}
const Bar = {
name: 'Bar',
extends: Base
}
const inlineHookSpy = vi.fn()
const vm = new Vue({
render(h) {
const data = {
staticClass: 'text-red',
hook: {
init: inlineHookSpy
}
}
return h('div', [h(Foo, data), h(Bar, data)])
}
}).$mount()
expect(vm.$el.textContent).toBe('FooBar')
expect(inlineHookSpy.mock.calls.length).toBe(2)
})
it('DOM props set throwing should not break app', done => {
const vm = new Vue({
data: {
n: Infinity
},
template: `
<div>
<progress :value="n"/>
{{ n }}
</div>
`
}).$mount()
expect(vm.$el.textContent).toMatch('Infinity')
vm.n = 1
waitForUpdate(() => {
expect(vm.$el.textContent).toMatch('1')
expect(vm.$el.textContent).not.toMatch('Infinity')
}).then(done)
})
it('should not throw when hydrated pending async component is patched by v-if="false"', done => {
const PendingAsyncComponent = () => new Promise(() => {})
const ssrAsyncComponent = document.createElement('div')
ssrAsyncComponent.setAttribute(SSR_ATTR, 'true')
const vm = new Vue({
data: {
visible: true
},
components: {
PendingAsyncComponent
},
template:
'<pending-async-component v-if="visible"></pending-async-component>'
}).$mount(ssrAsyncComponent)
vm.visible = false
vm.$nextTick(done)
})
it('should call directive\'s inserted hook correctly when template root is changed', done => {
let insertCalled = false
const vm = new Vue({
data: {
isOn: false
},
components: {
'v-switch': {
props: {
on: Boolean
},
template: `
<div v-if="on" key="on">On</div>
<div v-else key="off">Off</div>`
}
},
directives: {
foo: {
inserted() {
insertCalled = true
}
}
},
template: `
<transition-group>
<v-switch key="swicth" v-foo :on="isOn"/>
</transition-group>
`
}).$mount()
vm.isOn = true
insertCalled = false
waitForUpdate(() => {
expect(vm.$el.textContent).toMatch('On')
expect(insertCalled).toBe(true)
}).then(done)
})
})