| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- import Vue from 'vue'
- import VNode from 'core/vdom/vnode'
- import { patch } from 'web/runtime/patch'
- import { SSR_ATTR } from 'shared/constants'
- function createMockSSRDOM(innerHTML) {
- const dom = document.createElement('div')
- dom.setAttribute(SSR_ATTR, 'true')
- dom.innerHTML = innerHTML
- return dom
- }
- describe('vdom patch: hydration', () => {
- let vnode0
- beforeEach(() => {
- vnode0 = new VNode('p', { attrs: { id: '1' } }, [
- createTextVNode('hello world')
- ])
- patch(null, vnode0)
- })
- it('should hydrate elements when server-rendered DOM tree is same as virtual DOM tree', () => {
- const result: any[] = []
- function init(vnode) {
- result.push(vnode)
- }
- function createServerRenderedDOM() {
- const root = document.createElement('div')
- root.setAttribute(SSR_ATTR, 'true')
- const span = document.createElement('span')
- root.appendChild(span)
- const div = document.createElement('div')
- const child1 = document.createElement('span')
- const child2 = document.createElement('span')
- child1.textContent = 'hi'
- child2.textContent = 'ho'
- div.appendChild(child1)
- div.appendChild(child2)
- root.appendChild(div)
- return root
- }
- const node0 = createServerRenderedDOM()
- const vnode1 = new VNode('div', {}, [
- new VNode('span', {}),
- new VNode('div', { hook: { init } }, [
- new VNode('span', {}, [
- new VNode(undefined, undefined, undefined, 'hi')
- ]),
- new VNode('span', {}, [
- new VNode(undefined, undefined, undefined, 'ho')
- ])
- ])
- ])
- patch(node0, vnode1)
- expect(result.length).toBe(1)
- function traverseAndAssert(vnode, element) {
- expect(vnode.elm).toBe(element)
- if (vnode.children) {
- vnode.children.forEach((node, i) => {
- traverseAndAssert(node, element.childNodes[i])
- })
- }
- }
- // ensure vnodes are correctly associated with actual DOM
- traverseAndAssert(vnode1, node0)
- // check update
- const vnode2 = new VNode('div', { attrs: { id: 'foo' } }, [
- new VNode('span', { attrs: { id: 'bar' } }),
- new VNode('div', { hook: { init } }, [
- new VNode('span', {}),
- new VNode('span', {})
- ])
- ])
- patch(vnode1, vnode2)
- expect(node0.id).toBe('foo')
- expect(node0.children[0].id).toBe('bar')
- })
- it('should warn message that virtual DOM tree is not matching when hydrate element', () => {
- function createServerRenderedDOM() {
- const root = document.createElement('div')
- root.setAttribute(SSR_ATTR, 'true')
- const span = document.createElement('span')
- root.appendChild(span)
- const div = document.createElement('div')
- const child1 = document.createElement('span')
- div.appendChild(child1)
- root.appendChild(div)
- return root
- }
- const node0 = createServerRenderedDOM()
- const vnode1 = new VNode('div', {}, [
- new VNode('span', {}),
- new VNode('div', {}, [new VNode('span', {}), new VNode('span', {})])
- ])
- patch(node0, vnode1)
- expect(
- 'The client-side rendered virtual DOM tree is not matching'
- ).toHaveBeenWarned()
- })
- // component hydration is better off with a more e2e approach
- it('should hydrate components when server-rendered DOM tree is same as virtual DOM tree', done => {
- const dom = createMockSSRDOM(
- '<span>foo</span><div class="b a"><span>foo qux</span></div><!---->'
- )
- const originalNode1 = dom.children[0]
- const originalNode2 = dom.children[1]
- const vm = new Vue({
- template:
- '<div><span>{{msg}}</span><test class="a" :msg="msg"></test><p v-if="ok"></p></div>',
- data: {
- msg: 'foo',
- ok: false
- },
- components: {
- test: {
- props: ['msg'],
- data() {
- return { a: 'qux' }
- },
- template: '<div class="b"><span>{{msg}} {{a}}</span></div>'
- }
- }
- })
- expect(() => {
- vm.$mount(dom)
- }).not.toThrow()
- expect('not matching server-rendered content').not.toHaveBeenWarned()
- expect(vm.$el).toBe(dom)
- expect(vm.$children[0].$el).toBe(originalNode2)
- expect(vm.$el.children[0]).toBe(originalNode1)
- expect(vm.$el.children[1]).toBe(originalNode2)
- vm.msg = 'bar'
- waitForUpdate(() => {
- expect(vm.$el.innerHTML).toBe(
- '<span>bar</span><div class="b a"><span>bar qux</span></div><!---->'
- )
- vm.$children[0].a = 'ququx'
- })
- .then(() => {
- expect(vm.$el.innerHTML).toBe(
- '<span>bar</span><div class="b a"><span>bar ququx</span></div><!---->'
- )
- vm.ok = true
- })
- .then(() => {
- expect(vm.$el.innerHTML).toBe(
- '<span>bar</span><div class="b a"><span>bar ququx</span></div><p></p>'
- )
- })
- .then(done)
- })
- it('should warn failed hydration for non-matching DOM in child component', () => {
- const dom = createMockSSRDOM('<div><span></span></div>')
- new Vue({
- template: '<div><test></test></div>',
- components: {
- test: {
- template: '<div><a></a></div>'
- }
- }
- }).$mount(dom)
- expect('not matching server-rendered content').toHaveBeenWarned()
- })
- it('should warn failed hydration when component is not properly registered', () => {
- const dom = createMockSSRDOM('<div><foo></foo></div>')
- new Vue({
- template: '<div><foo></foo></div>'
- }).$mount(dom)
- expect('not matching server-rendered content').toHaveBeenWarned()
- expect('Unknown custom element: <foo>').toHaveBeenWarned()
- })
- it('should overwrite textNodes in the correct position but with mismatching text without warning', () => {
- const dom = createMockSSRDOM('<div><span>foo</span></div>')
- new Vue({
- template: '<div><test></test></div>',
- components: {
- test: {
- data() {
- return { a: 'qux' }
- },
- template: '<div><span>{{a}}</span></div>'
- }
- }
- }).$mount(dom)
- expect('not matching server-rendered content').not.toHaveBeenWarned()
- expect(dom.querySelector('span').textContent).toBe('qux')
- })
- it('should pick up elements with no children and populate without warning', done => {
- const dom = createMockSSRDOM('<div><span></span></div>')
- const span = dom.querySelector('span')
- const vm = new Vue({
- template: '<div><test></test></div>',
- components: {
- test: {
- data() {
- return { a: 'qux' }
- },
- template: '<div><span>{{a}}</span></div>'
- }
- }
- }).$mount(dom)
- expect('not matching server-rendered content').not.toHaveBeenWarned()
- expect(span).toBe(vm.$el.querySelector('span'))
- expect(vm.$el.innerHTML).toBe('<div><span>qux</span></div>')
- vm.$children[0].a = 'foo'
- waitForUpdate(() => {
- expect(vm.$el.innerHTML).toBe('<div><span>foo</span></div>')
- }).then(done)
- })
- it('should hydrate async component', done => {
- const dom = createMockSSRDOM('<span>foo</span>')
- const span = dom.querySelector('span')
- const Foo = resolve =>
- setTimeout(() => {
- resolve({
- data: () => ({ msg: 'foo' }),
- template: `<span>{{ msg }}</span>`
- })
- }, 0)
- const vm = new Vue({
- template: '<div><foo ref="foo" /></div>',
- components: { Foo }
- }).$mount(dom)
- expect('not matching server-rendered content').not.toHaveBeenWarned()
- expect(dom.innerHTML).toBe('<span>foo</span>')
- expect(vm.$refs.foo).toBeUndefined()
- setTimeout(() => {
- expect(dom.innerHTML).toBe('<span>foo</span>')
- expect(vm.$refs.foo).not.toBeUndefined()
- vm.$refs.foo.msg = 'bar'
- waitForUpdate(() => {
- expect(dom.innerHTML).toBe('<span>bar</span>')
- expect(dom.querySelector('span')).toBe(span)
- }).then(done)
- }, 50)
- })
- it('should hydrate async component without showing loading', done => {
- const dom = createMockSSRDOM('<span>foo</span>')
- const span = dom.querySelector('span')
- const Foo = () => ({
- component: new Promise(resolve => {
- setTimeout(() => {
- resolve({
- data: () => ({ msg: 'foo' }),
- template: `<span>{{ msg }}</span>`
- })
- }, 10)
- }),
- delay: 1,
- loading: {
- render: h => h('span', 'loading')
- }
- })
- const vm = new Vue({
- template: '<div><foo ref="foo" /></div>',
- components: { Foo }
- }).$mount(dom)
- expect('not matching server-rendered content').not.toHaveBeenWarned()
- expect(dom.innerHTML).toBe('<span>foo</span>')
- expect(vm.$refs.foo).toBeUndefined()
- setTimeout(() => {
- expect(dom.innerHTML).toBe('<span>foo</span>')
- }, 2)
- setTimeout(() => {
- expect(dom.innerHTML).toBe('<span>foo</span>')
- expect(vm.$refs.foo).not.toBeUndefined()
- vm.$refs.foo.msg = 'bar'
- waitForUpdate(() => {
- expect(dom.innerHTML).toBe('<span>bar</span>')
- expect(dom.querySelector('span')).toBe(span)
- }).then(done)
- }, 50)
- })
- it('should hydrate async component by replacing DOM if error occurs', done => {
- const dom = createMockSSRDOM('<span>foo</span>')
- const Foo = () => ({
- component: new Promise((resolve, reject) => {
- setTimeout(() => {
- reject('something went wrong')
- }, 10)
- }),
- error: {
- render: h => h('span', 'error')
- }
- })
- new Vue({
- template: '<div><foo ref="foo" /></div>',
- components: { Foo }
- }).$mount(dom)
- expect('not matching server-rendered content').not.toHaveBeenWarned()
- expect(dom.innerHTML).toBe('<span>foo</span>')
- setTimeout(() => {
- expect('Failed to resolve async').toHaveBeenWarned()
- expect(dom.innerHTML).toBe('<span>error</span>')
- done()
- }, 50)
- })
- it('should hydrate v-html with children', () => {
- const dom = createMockSSRDOM('<span>foo</span>')
- new Vue({
- data: {
- html: `<span>foo</span>`
- },
- template: `<div v-html="html">hello</div>`
- }).$mount(dom)
- expect('not matching server-rendered content').not.toHaveBeenWarned()
- })
- it('should warn mismatching v-html', () => {
- const dom = createMockSSRDOM('<span>bar</span>')
- new Vue({
- data: {
- html: `<span>foo</span>`
- },
- template: `<div v-html="html">hello</div>`
- }).$mount(dom)
- expect('not matching server-rendered content').toHaveBeenWarned()
- })
- it('should hydrate with adjacent text nodes from array children (e.g. slots)', () => {
- const dom = createMockSSRDOM('<div>foo</div> hello')
- new Vue({
- template: `<test>hello</test>`,
- components: {
- test: {
- template: `
- <div>
- <div>foo</div>
- <slot/>
- </div>
- `
- }
- }
- }).$mount(dom)
- expect('not matching server-rendered content').not.toHaveBeenWarned()
- })
- // #7063
- it('should properly initialize dynamic style bindings for future updates', done => {
- const dom = createMockSSRDOM('<div style="padding-left:0px"></div>')
- const vm = new Vue({
- data: {
- style: { paddingLeft: '0px' }
- },
- template: `<div><div :style="style"></div></div>`
- }).$mount(dom)
- // should update
- vm.style.paddingLeft = '100px'
- waitForUpdate(() => {
- expect(dom.children[0].style.paddingLeft).toBe('100px')
- }).then(done)
- })
- it('should properly initialize dynamic class bindings for future updates', done => {
- const dom = createMockSSRDOM('<div class="foo bar"></div>')
- const vm = new Vue({
- data: {
- cls: [{ foo: true }, 'bar']
- },
- template: `<div><div :class="cls"></div></div>`
- }).$mount(dom)
- // should update
- vm.cls[0].foo = false
- waitForUpdate(() => {
- expect(dom.children[0].className).toBe('bar')
- }).then(done)
- })
- })
|