hydration.spec.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  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. function createMockSSRDOM (innerHTML) {
  6. const dom = document.createElement('div')
  7. dom.setAttribute(SSR_ATTR, 'true')
  8. dom.innerHTML = innerHTML
  9. return dom
  10. }
  11. describe('vdom patch: hydration', () => {
  12. let vnode0
  13. beforeEach(() => {
  14. vnode0 = new VNode('p', { attrs: { id: '1' }}, [createTextVNode('hello world')])
  15. patch(null, vnode0)
  16. })
  17. it('should hydrate elements when server-rendered DOM tree is same as virtual DOM tree', () => {
  18. const result = []
  19. function init (vnode) { result.push(vnode) }
  20. function createServerRenderedDOM () {
  21. const root = document.createElement('div')
  22. root.setAttribute(SSR_ATTR, 'true')
  23. const span = document.createElement('span')
  24. root.appendChild(span)
  25. const div = document.createElement('div')
  26. const child1 = document.createElement('span')
  27. const child2 = document.createElement('span')
  28. child1.textContent = 'hi'
  29. child2.textContent = 'ho'
  30. div.appendChild(child1)
  31. div.appendChild(child2)
  32. root.appendChild(div)
  33. return root
  34. }
  35. const node0 = createServerRenderedDOM()
  36. const vnode1 = new VNode('div', {}, [
  37. new VNode('span', {}),
  38. new VNode('div', { hook: { init }}, [
  39. new VNode('span', {}, [new VNode(undefined, undefined, undefined, 'hi')]),
  40. new VNode('span', {}, [new VNode(undefined, undefined, undefined, 'ho')])
  41. ])
  42. ])
  43. patch(node0, vnode1)
  44. expect(result.length).toBe(1)
  45. function traverseAndAssert (vnode, element) {
  46. expect(vnode.elm).toBe(element)
  47. if (vnode.children) {
  48. vnode.children.forEach((node, i) => {
  49. traverseAndAssert(node, element.childNodes[i])
  50. })
  51. }
  52. }
  53. // ensure vnodes are correctly associated with actual DOM
  54. traverseAndAssert(vnode1, node0)
  55. // check update
  56. const vnode2 = new VNode('div', { attrs: { id: 'foo' }}, [
  57. new VNode('span', { attrs: { id: 'bar' }}),
  58. new VNode('div', { hook: { init }}, [
  59. new VNode('span', {}),
  60. new VNode('span', {})
  61. ])
  62. ])
  63. patch(vnode1, vnode2)
  64. expect(node0.id).toBe('foo')
  65. expect(node0.children[0].id).toBe('bar')
  66. })
  67. it('should warn message that virtual DOM tree is not matching when hydrate element', () => {
  68. function createServerRenderedDOM () {
  69. const root = document.createElement('div')
  70. root.setAttribute(SSR_ATTR, 'true')
  71. const span = document.createElement('span')
  72. root.appendChild(span)
  73. const div = document.createElement('div')
  74. const child1 = document.createElement('span')
  75. div.appendChild(child1)
  76. root.appendChild(div)
  77. return root
  78. }
  79. const node0 = createServerRenderedDOM()
  80. const vnode1 = new VNode('div', {}, [
  81. new VNode('span', {}),
  82. new VNode('div', {}, [
  83. new VNode('span', {}),
  84. new VNode('span', {})
  85. ])
  86. ])
  87. patch(node0, vnode1)
  88. expect('The client-side rendered virtual DOM tree is not matching').toHaveBeenWarned()
  89. })
  90. // component hydration is better off with a more e2e approach
  91. it('should hydrate components when server-rendered DOM tree is same as virtual DOM tree', done => {
  92. const dom = createMockSSRDOM('<span>foo</span><div class="b a"><span>foo qux</span></div><!---->')
  93. const originalNode1 = dom.children[0]
  94. const originalNode2 = dom.children[1]
  95. const vm = new Vue({
  96. template: '<div><span>{{msg}}</span><test class="a" :msg="msg"></test><p v-if="ok"></p></div>',
  97. data: {
  98. msg: 'foo',
  99. ok: false
  100. },
  101. components: {
  102. test: {
  103. props: ['msg'],
  104. data () {
  105. return { a: 'qux' }
  106. },
  107. template: '<div class="b"><span>{{msg}} {{a}}</span></div>'
  108. }
  109. }
  110. })
  111. expect(() => { vm.$mount(dom) }).not.toThrow()
  112. expect('not matching server-rendered content').not.toHaveBeenWarned()
  113. expect(vm.$el).toBe(dom)
  114. expect(vm.$children[0].$el).toBe(originalNode2)
  115. expect(vm.$el.children[0]).toBe(originalNode1)
  116. expect(vm.$el.children[1]).toBe(originalNode2)
  117. vm.msg = 'bar'
  118. waitForUpdate(() => {
  119. expect(vm.$el.innerHTML).toBe('<span>bar</span><div class="b a"><span>bar qux</span></div><!---->')
  120. vm.$children[0].a = 'ququx'
  121. }).then(() => {
  122. expect(vm.$el.innerHTML).toBe('<span>bar</span><div class="b a"><span>bar ququx</span></div><!---->')
  123. vm.ok = true
  124. }).then(() => {
  125. expect(vm.$el.innerHTML).toBe('<span>bar</span><div class="b a"><span>bar ququx</span></div><p></p>')
  126. }).then(done)
  127. })
  128. it('should warn failed hydration for non-matching DOM in child component', () => {
  129. const dom = createMockSSRDOM('<div><span></span></div>')
  130. new Vue({
  131. template: '<div><test></test></div>',
  132. components: {
  133. test: {
  134. template: '<div><a></a></div>'
  135. }
  136. }
  137. }).$mount(dom)
  138. expect('not matching server-rendered content').toHaveBeenWarned()
  139. })
  140. it('should overwrite textNodes in the correct position but with mismatching text without warning', () => {
  141. const dom = createMockSSRDOM('<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 = createMockSSRDOM('<div><span></span></div>')
  158. const span = dom.querySelector('span')
  159. const vm = new Vue({
  160. template: '<div><test></test></div>',
  161. components: {
  162. test: {
  163. data () {
  164. return { a: 'qux' }
  165. },
  166. template: '<div><span>{{a}}</span></div>'
  167. }
  168. }
  169. }).$mount(dom)
  170. expect('not matching server-rendered content').not.toHaveBeenWarned()
  171. expect(span).toBe(vm.$el.querySelector('span'))
  172. expect(vm.$el.innerHTML).toBe('<div><span>qux</span></div>')
  173. vm.$children[0].a = 'foo'
  174. waitForUpdate(() => {
  175. expect(vm.$el.innerHTML).toBe('<div><span>foo</span></div>')
  176. }).then(done)
  177. })
  178. it('should hydrate async component', done => {
  179. const dom = createMockSSRDOM('<span>foo</span>')
  180. const span = dom.querySelector('span')
  181. const Foo = resolve => setTimeout(() => {
  182. resolve({
  183. data: () => ({ msg: 'foo' }),
  184. template: `<span>{{ msg }}</span>`
  185. })
  186. }, 0)
  187. const vm = new Vue({
  188. template: '<div><foo ref="foo" /></div>',
  189. components: { Foo }
  190. }).$mount(dom)
  191. expect('not matching server-rendered content').not.toHaveBeenWarned()
  192. expect(dom.innerHTML).toBe('<span>foo</span>')
  193. expect(vm.$refs.foo).toBeUndefined()
  194. setTimeout(() => {
  195. expect(dom.innerHTML).toBe('<span>foo</span>')
  196. expect(vm.$refs.foo).not.toBeUndefined()
  197. vm.$refs.foo.msg = 'bar'
  198. waitForUpdate(() => {
  199. expect(dom.innerHTML).toBe('<span>bar</span>')
  200. expect(dom.querySelector('span')).toBe(span)
  201. }).then(done)
  202. }, 0)
  203. })
  204. it('should hydrate async component without showing loading', done => {
  205. const dom = createMockSSRDOM('<span>foo</span>')
  206. const span = dom.querySelector('span')
  207. const Foo = () => ({
  208. component: new Promise(resolve => {
  209. setTimeout(() => {
  210. resolve({
  211. data: () => ({ msg: 'foo' }),
  212. template: `<span>{{ msg }}</span>`
  213. })
  214. }, 10)
  215. }),
  216. delay: 1,
  217. loading: {
  218. render: h => h('span', 'loading')
  219. }
  220. })
  221. const vm = new Vue({
  222. template: '<div><foo ref="foo" /></div>',
  223. components: { Foo }
  224. }).$mount(dom)
  225. expect('not matching server-rendered content').not.toHaveBeenWarned()
  226. expect(dom.innerHTML).toBe('<span>foo</span>')
  227. expect(vm.$refs.foo).toBeUndefined()
  228. setTimeout(() => {
  229. expect(dom.innerHTML).toBe('<span>foo</span>')
  230. }, 1)
  231. setTimeout(() => {
  232. expect(dom.innerHTML).toBe('<span>foo</span>')
  233. expect(vm.$refs.foo).not.toBeUndefined()
  234. vm.$refs.foo.msg = 'bar'
  235. waitForUpdate(() => {
  236. expect(dom.innerHTML).toBe('<span>bar</span>')
  237. expect(dom.querySelector('span')).toBe(span)
  238. }).then(done)
  239. }, 10)
  240. })
  241. it('should hydrate async component by replacing DOM if error occurs', done => {
  242. const dom = createMockSSRDOM('<span>foo</span>')
  243. const Foo = () => ({
  244. component: new Promise((resolve, reject) => {
  245. setTimeout(() => {
  246. reject('something went wrong')
  247. }, 10)
  248. }),
  249. error: {
  250. render: h => h('span', 'error')
  251. }
  252. })
  253. new Vue({
  254. template: '<div><foo ref="foo" /></div>',
  255. components: { Foo }
  256. }).$mount(dom)
  257. expect('not matching server-rendered content').not.toHaveBeenWarned()
  258. expect(dom.innerHTML).toBe('<span>foo</span>')
  259. setTimeout(() => {
  260. expect('Failed to resolve async').toHaveBeenWarned()
  261. expect(dom.innerHTML).toBe('<span>error</span>')
  262. done()
  263. }, 10)
  264. })
  265. })