hydration.spec.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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 warn failed hydration when component is not properly registered', () => {
  141. const dom = createMockSSRDOM('<div><foo></foo></div>')
  142. new Vue({
  143. template: '<div><foo></foo></div>'
  144. }).$mount(dom)
  145. expect('not matching server-rendered content').toHaveBeenWarned()
  146. expect('Unknown custom element: <foo>').toHaveBeenWarned()
  147. })
  148. it('should overwrite textNodes in the correct position but with mismatching text without warning', () => {
  149. const dom = createMockSSRDOM('<div><span>foo</span></div>')
  150. new Vue({
  151. template: '<div><test></test></div>',
  152. components: {
  153. test: {
  154. data () {
  155. return { a: 'qux' }
  156. },
  157. template: '<div><span>{{a}}</span></div>'
  158. }
  159. }
  160. }).$mount(dom)
  161. expect('not matching server-rendered content').not.toHaveBeenWarned()
  162. expect(dom.querySelector('span').textContent).toBe('qux')
  163. })
  164. it('should pick up elements with no children and populate without warning', done => {
  165. const dom = createMockSSRDOM('<div><span></span></div>')
  166. const span = dom.querySelector('span')
  167. const vm = new Vue({
  168. template: '<div><test></test></div>',
  169. components: {
  170. test: {
  171. data () {
  172. return { a: 'qux' }
  173. },
  174. template: '<div><span>{{a}}</span></div>'
  175. }
  176. }
  177. }).$mount(dom)
  178. expect('not matching server-rendered content').not.toHaveBeenWarned()
  179. expect(span).toBe(vm.$el.querySelector('span'))
  180. expect(vm.$el.innerHTML).toBe('<div><span>qux</span></div>')
  181. vm.$children[0].a = 'foo'
  182. waitForUpdate(() => {
  183. expect(vm.$el.innerHTML).toBe('<div><span>foo</span></div>')
  184. }).then(done)
  185. })
  186. it('should hydrate async component', done => {
  187. const dom = createMockSSRDOM('<span>foo</span>')
  188. const span = dom.querySelector('span')
  189. const Foo = resolve => setTimeout(() => {
  190. resolve({
  191. data: () => ({ msg: 'foo' }),
  192. template: `<span>{{ msg }}</span>`
  193. })
  194. }, 0)
  195. const vm = new Vue({
  196. template: '<div><foo ref="foo" /></div>',
  197. components: { Foo }
  198. }).$mount(dom)
  199. expect('not matching server-rendered content').not.toHaveBeenWarned()
  200. expect(dom.innerHTML).toBe('<span>foo</span>')
  201. expect(vm.$refs.foo).toBeUndefined()
  202. setTimeout(() => {
  203. expect(dom.innerHTML).toBe('<span>foo</span>')
  204. expect(vm.$refs.foo).not.toBeUndefined()
  205. vm.$refs.foo.msg = 'bar'
  206. waitForUpdate(() => {
  207. expect(dom.innerHTML).toBe('<span>bar</span>')
  208. expect(dom.querySelector('span')).toBe(span)
  209. }).then(done)
  210. }, 50)
  211. })
  212. it('should hydrate async component without showing loading', done => {
  213. const dom = createMockSSRDOM('<span>foo</span>')
  214. const span = dom.querySelector('span')
  215. const Foo = () => ({
  216. component: new Promise(resolve => {
  217. setTimeout(() => {
  218. resolve({
  219. data: () => ({ msg: 'foo' }),
  220. template: `<span>{{ msg }}</span>`
  221. })
  222. }, 10)
  223. }),
  224. delay: 1,
  225. loading: {
  226. render: h => h('span', 'loading')
  227. }
  228. })
  229. const vm = new Vue({
  230. template: '<div><foo ref="foo" /></div>',
  231. components: { Foo }
  232. }).$mount(dom)
  233. expect('not matching server-rendered content').not.toHaveBeenWarned()
  234. expect(dom.innerHTML).toBe('<span>foo</span>')
  235. expect(vm.$refs.foo).toBeUndefined()
  236. setTimeout(() => {
  237. expect(dom.innerHTML).toBe('<span>foo</span>')
  238. }, 2)
  239. setTimeout(() => {
  240. expect(dom.innerHTML).toBe('<span>foo</span>')
  241. expect(vm.$refs.foo).not.toBeUndefined()
  242. vm.$refs.foo.msg = 'bar'
  243. waitForUpdate(() => {
  244. expect(dom.innerHTML).toBe('<span>bar</span>')
  245. expect(dom.querySelector('span')).toBe(span)
  246. }).then(done)
  247. }, 50)
  248. })
  249. it('should hydrate async component by replacing DOM if error occurs', done => {
  250. const dom = createMockSSRDOM('<span>foo</span>')
  251. const Foo = () => ({
  252. component: new Promise((resolve, reject) => {
  253. setTimeout(() => {
  254. reject('something went wrong')
  255. }, 10)
  256. }),
  257. error: {
  258. render: h => h('span', 'error')
  259. }
  260. })
  261. new Vue({
  262. template: '<div><foo ref="foo" /></div>',
  263. components: { Foo }
  264. }).$mount(dom)
  265. expect('not matching server-rendered content').not.toHaveBeenWarned()
  266. expect(dom.innerHTML).toBe('<span>foo</span>')
  267. setTimeout(() => {
  268. expect('Failed to resolve async').toHaveBeenWarned()
  269. expect(dom.innerHTML).toBe('<span>error</span>')
  270. done()
  271. }, 50)
  272. })
  273. it('should hydrate v-html with children', () => {
  274. const dom = createMockSSRDOM('<span>foo</span>')
  275. new Vue({
  276. data: {
  277. html: `<span>foo</span>`
  278. },
  279. template: `<div v-html="html">hello</div>`
  280. }).$mount(dom)
  281. expect('not matching server-rendered content').not.toHaveBeenWarned()
  282. })
  283. it('should warn mismatching v-html', () => {
  284. const dom = createMockSSRDOM('<span>bar</span>')
  285. new Vue({
  286. data: {
  287. html: `<span>foo</span>`
  288. },
  289. template: `<div v-html="html">hello</div>`
  290. }).$mount(dom)
  291. expect('not matching server-rendered content').toHaveBeenWarned()
  292. })
  293. it('should hydrate with adjacent text nodes from array children (e.g. slots)', () => {
  294. const dom = createMockSSRDOM('<div>foo</div> hello')
  295. new Vue({
  296. template: `<test>hello</test>`,
  297. components: {
  298. test: {
  299. template: `
  300. <div>
  301. <div>foo</div>
  302. <slot/>
  303. </div>
  304. `
  305. }
  306. }
  307. }).$mount(dom)
  308. expect('not matching server-rendered content').not.toHaveBeenWarned()
  309. })
  310. // #7063
  311. it('should properly initialize dynamic style bindings for future updates', done => {
  312. const dom = createMockSSRDOM('<div style="padding-left:0px"></div>')
  313. const vm = new Vue({
  314. data: {
  315. style: { paddingLeft: '0px' }
  316. },
  317. template: `<div><div :style="style"></div></div>`
  318. }).$mount(dom)
  319. // should update
  320. vm.style.paddingLeft = '100px'
  321. waitForUpdate(() => {
  322. expect(dom.children[0].style.paddingLeft).toBe('100px')
  323. }).then(done)
  324. })
  325. it('should properly initialize dynamic class bindings for future updates', done => {
  326. const dom = createMockSSRDOM('<div class="foo bar"></div>')
  327. const vm = new Vue({
  328. data: {
  329. cls: [{ foo: true }, 'bar']
  330. },
  331. template: `<div><div :class="cls"></div></div>`
  332. }).$mount(dom)
  333. // should update
  334. vm.cls[0].foo = false
  335. waitForUpdate(() => {
  336. expect(dom.children[0].className).toBe('bar')
  337. }).then(done)
  338. })
  339. })