hydration.spec.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import Vue from 'vue'
  2. import VNode from 'core/vdom/vnode'
  3. import { patch } from 'web/runtime/patch'
  4. import { SSR_ATTR } from 'shared/constants'
  5. describe('vdom patch: hydration', () => {
  6. let vnode0
  7. beforeEach(() => {
  8. vnode0 = new VNode('p', { attrs: { id: '1' }}, [createTextVNode('hello world')])
  9. patch(null, vnode0)
  10. })
  11. it('should hydrate elements when server-rendered DOM tree is same as virtual DOM tree', () => {
  12. const result = []
  13. function init (vnode) { result.push(vnode) }
  14. function createServerRenderedDOM () {
  15. const root = document.createElement('div')
  16. root.setAttribute(SSR_ATTR, 'true')
  17. const span = document.createElement('span')
  18. root.appendChild(span)
  19. const div = document.createElement('div')
  20. const child1 = document.createElement('span')
  21. const child2 = document.createElement('span')
  22. child1.textContent = 'hi'
  23. child2.textContent = 'ho'
  24. div.appendChild(child1)
  25. div.appendChild(child2)
  26. root.appendChild(div)
  27. return root
  28. }
  29. const node0 = createServerRenderedDOM()
  30. const vnode1 = new VNode('div', {}, [
  31. new VNode('span', {}),
  32. new VNode('div', { hook: { init }}, [
  33. new VNode('span', {}, [new VNode(undefined, undefined, undefined, 'hi')]),
  34. new VNode('span', {}, [new VNode(undefined, undefined, undefined, 'ho')])
  35. ])
  36. ])
  37. patch(node0, vnode1)
  38. expect(result.length).toBe(1)
  39. function traverseAndAssert (vnode, element) {
  40. expect(vnode.elm).toBe(element)
  41. if (vnode.children) {
  42. vnode.children.forEach((node, i) => {
  43. traverseAndAssert(node, element.childNodes[i])
  44. })
  45. }
  46. }
  47. // ensure vnodes are correctly associated with actual DOM
  48. traverseAndAssert(vnode1, node0)
  49. // check update
  50. const vnode2 = new VNode('div', { attrs: { id: 'foo' }}, [
  51. new VNode('span', { attrs: { id: 'bar' }}),
  52. new VNode('div', { hook: { init }}, [
  53. new VNode('span', {}),
  54. new VNode('span', {})
  55. ])
  56. ])
  57. patch(vnode1, vnode2)
  58. expect(node0.id).toBe('foo')
  59. expect(node0.children[0].id).toBe('bar')
  60. })
  61. it('should warn message that virtual DOM tree is not matching when hydrate element', () => {
  62. function createServerRenderedDOM () {
  63. const root = document.createElement('div')
  64. root.setAttribute(SSR_ATTR, 'true')
  65. const span = document.createElement('span')
  66. root.appendChild(span)
  67. const div = document.createElement('div')
  68. const child1 = document.createElement('span')
  69. div.appendChild(child1)
  70. root.appendChild(div)
  71. return root
  72. }
  73. const node0 = createServerRenderedDOM()
  74. const vnode1 = new VNode('div', {}, [
  75. new VNode('span', {}),
  76. new VNode('div', {}, [
  77. new VNode('span', {}),
  78. new VNode('span', {})
  79. ])
  80. ])
  81. patch(node0, vnode1)
  82. expect('The client-side rendered virtual DOM tree is not matching').toHaveBeenWarned()
  83. })
  84. // component hydration is better off with a more e2e approach
  85. it('should hydrate components when server-rendered DOM tree is same as virtual DOM tree', done => {
  86. const dom = document.createElement('div')
  87. dom.setAttribute(SSR_ATTR, 'true')
  88. dom.innerHTML = '<span>foo</span><div class="b a"><span>foo qux</span></div><!---->'
  89. const originalNode1 = dom.children[0]
  90. const originalNode2 = dom.children[1]
  91. const vm = new Vue({
  92. template: '<div><span>{{msg}}</span><test class="a" :msg="msg"></test><p v-if="ok"></p></div>',
  93. data: {
  94. msg: 'foo',
  95. ok: false
  96. },
  97. components: {
  98. test: {
  99. props: ['msg'],
  100. data () {
  101. return { a: 'qux' }
  102. },
  103. template: '<div class="b"><span>{{msg}} {{a}}</span></div>'
  104. }
  105. }
  106. })
  107. expect(() => { vm.$mount(dom) }).not.toThrow()
  108. expect('not matching server-rendered content').not.toHaveBeenWarned()
  109. expect(vm.$el).toBe(dom)
  110. expect(vm.$children[0].$el).toBe(originalNode2)
  111. expect(vm.$el.children[0]).toBe(originalNode1)
  112. expect(vm.$el.children[1]).toBe(originalNode2)
  113. vm.msg = 'bar'
  114. waitForUpdate(() => {
  115. expect(vm.$el.innerHTML).toBe('<span>bar</span><div class="b a"><span>bar qux</span></div><!---->')
  116. vm.$children[0].a = 'ququx'
  117. }).then(() => {
  118. expect(vm.$el.innerHTML).toBe('<span>bar</span><div class="b a"><span>bar ququx</span></div><!---->')
  119. vm.ok = true
  120. }).then(() => {
  121. expect(vm.$el.innerHTML).toBe('<span>bar</span><div class="b a"><span>bar ququx</span></div><p></p>')
  122. }).then(done)
  123. })
  124. it('should warn failed hydration for non-matching DOM in child component', () => {
  125. const dom = document.createElement('div')
  126. dom.setAttribute(SSR_ATTR, 'true')
  127. dom.innerHTML = '<div><span></span></div>'
  128. new Vue({
  129. template: '<div><test></test></div>',
  130. components: {
  131. test: {
  132. template: '<div><a></a></div>'
  133. }
  134. }
  135. }).$mount(dom)
  136. expect('not matching server-rendered content').toHaveBeenWarned()
  137. })
  138. it('should overwrite textNodes in the correct position but with mismatching text without warning', () => {
  139. const dom = document.createElement('div')
  140. dom.setAttribute(SSR_ATTR, 'true')
  141. dom.innerHTML = '<div><span>foo</span></div>'
  142. new Vue({
  143. template: '<div><test></test></div>',
  144. components: {
  145. test: {
  146. data () {
  147. return { a: 'qux' }
  148. },
  149. template: '<div><span>{{a}}</span></div>'
  150. }
  151. }
  152. }).$mount(dom)
  153. expect('not matching server-rendered content').not.toHaveBeenWarned()
  154. expect(dom.querySelector('span').textContent).toBe('qux')
  155. })
  156. it('should pick up elements with no children and populate without warning', done => {
  157. const dom = document.createElement('div')
  158. dom.setAttribute(SSR_ATTR, 'true')
  159. dom.innerHTML = '<div><span></span></div>'
  160. const span = dom.querySelector('span')
  161. const vm = new Vue({
  162. template: '<div><test></test></div>',
  163. components: {
  164. test: {
  165. data () {
  166. return { a: 'qux' }
  167. },
  168. template: '<div><span>{{a}}</span></div>'
  169. }
  170. }
  171. }).$mount(dom)
  172. expect('not matching server-rendered content').not.toHaveBeenWarned()
  173. expect(span).toBe(vm.$el.querySelector('span'))
  174. expect(vm.$el.innerHTML).toBe('<div><span>qux</span></div>')
  175. vm.$children[0].a = 'foo'
  176. waitForUpdate(() => {
  177. expect(vm.$el.innerHTML).toBe('<div><span>foo</span></div>')
  178. }).then(done)
  179. })
  180. })