hydration.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  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' } }, [
  15. createTextVNode('hello world')
  16. ])
  17. patch(null, vnode0)
  18. })
  19. it('should hydrate elements when server-rendered DOM tree is same as virtual DOM tree', () => {
  20. const result: any[] = []
  21. function init(vnode) {
  22. result.push(vnode)
  23. }
  24. function createServerRenderedDOM() {
  25. const root = document.createElement('div')
  26. root.setAttribute(SSR_ATTR, 'true')
  27. const span = document.createElement('span')
  28. root.appendChild(span)
  29. const div = document.createElement('div')
  30. const child1 = document.createElement('span')
  31. const child2 = document.createElement('span')
  32. child1.textContent = 'hi'
  33. child2.textContent = 'ho'
  34. div.appendChild(child1)
  35. div.appendChild(child2)
  36. root.appendChild(div)
  37. return root
  38. }
  39. const node0 = createServerRenderedDOM()
  40. const vnode1 = new VNode('div', {}, [
  41. new VNode('span', {}),
  42. new VNode('div', { hook: { init } }, [
  43. new VNode('span', {}, [
  44. new VNode(undefined, undefined, undefined, 'hi')
  45. ]),
  46. new VNode('span', {}, [
  47. new VNode(undefined, undefined, undefined, 'ho')
  48. ])
  49. ])
  50. ])
  51. patch(node0, vnode1)
  52. expect(result.length).toBe(1)
  53. function traverseAndAssert(vnode, element) {
  54. expect(vnode.elm).toBe(element)
  55. if (vnode.children) {
  56. vnode.children.forEach((node, i) => {
  57. traverseAndAssert(node, element.childNodes[i])
  58. })
  59. }
  60. }
  61. // ensure vnodes are correctly associated with actual DOM
  62. traverseAndAssert(vnode1, node0)
  63. // check update
  64. const vnode2 = new VNode('div', { attrs: { id: 'foo' } }, [
  65. new VNode('span', { attrs: { id: 'bar' } }),
  66. new VNode('div', { hook: { init } }, [
  67. new VNode('span', {}),
  68. new VNode('span', {})
  69. ])
  70. ])
  71. patch(vnode1, vnode2)
  72. expect(node0.id).toBe('foo')
  73. expect(node0.children[0].id).toBe('bar')
  74. })
  75. it('should warn message that virtual DOM tree is not matching when hydrate element', () => {
  76. function createServerRenderedDOM() {
  77. const root = document.createElement('div')
  78. root.setAttribute(SSR_ATTR, 'true')
  79. const span = document.createElement('span')
  80. root.appendChild(span)
  81. const div = document.createElement('div')
  82. const child1 = document.createElement('span')
  83. div.appendChild(child1)
  84. root.appendChild(div)
  85. return root
  86. }
  87. const node0 = createServerRenderedDOM()
  88. const vnode1 = new VNode('div', {}, [
  89. new VNode('span', {}),
  90. new VNode('div', {}, [new VNode('span', {}), new VNode('span', {})])
  91. ])
  92. patch(node0, vnode1)
  93. expect(
  94. 'The client-side rendered virtual DOM tree is not matching'
  95. ).toHaveBeenWarned()
  96. })
  97. // component hydration is better off with a more e2e approach
  98. it('should hydrate components when server-rendered DOM tree is same as virtual DOM tree', done => {
  99. const dom = createMockSSRDOM(
  100. '<span>foo</span><div class="b a"><span>foo qux</span></div><!---->'
  101. )
  102. const originalNode1 = dom.children[0]
  103. const originalNode2 = dom.children[1]
  104. const vm = new Vue({
  105. template:
  106. '<div><span>{{msg}}</span><test class="a" :msg="msg"></test><p v-if="ok"></p></div>',
  107. data: {
  108. msg: 'foo',
  109. ok: false
  110. },
  111. components: {
  112. test: {
  113. props: ['msg'],
  114. data() {
  115. return { a: 'qux' }
  116. },
  117. template: '<div class="b"><span>{{msg}} {{a}}</span></div>'
  118. }
  119. }
  120. })
  121. expect(() => {
  122. vm.$mount(dom)
  123. }).not.toThrow()
  124. expect('not matching server-rendered content').not.toHaveBeenWarned()
  125. expect(vm.$el).toBe(dom)
  126. expect(vm.$children[0].$el).toBe(originalNode2)
  127. expect(vm.$el.children[0]).toBe(originalNode1)
  128. expect(vm.$el.children[1]).toBe(originalNode2)
  129. vm.msg = 'bar'
  130. waitForUpdate(() => {
  131. expect(vm.$el.innerHTML).toBe(
  132. '<span>bar</span><div class="b a"><span>bar qux</span></div><!---->'
  133. )
  134. vm.$children[0].a = 'ququx'
  135. })
  136. .then(() => {
  137. expect(vm.$el.innerHTML).toBe(
  138. '<span>bar</span><div class="b a"><span>bar ququx</span></div><!---->'
  139. )
  140. vm.ok = true
  141. })
  142. .then(() => {
  143. expect(vm.$el.innerHTML).toBe(
  144. '<span>bar</span><div class="b a"><span>bar ququx</span></div><p></p>'
  145. )
  146. })
  147. .then(done)
  148. })
  149. it('should warn failed hydration for non-matching DOM in child component', () => {
  150. const dom = createMockSSRDOM('<div><span></span></div>')
  151. new Vue({
  152. template: '<div><test></test></div>',
  153. components: {
  154. test: {
  155. template: '<div><a></a></div>'
  156. }
  157. }
  158. }).$mount(dom)
  159. expect('not matching server-rendered content').toHaveBeenWarned()
  160. })
  161. it('should warn failed hydration when component is not properly registered', () => {
  162. const dom = createMockSSRDOM('<div><foo></foo></div>')
  163. new Vue({
  164. template: '<div><foo></foo></div>'
  165. }).$mount(dom)
  166. expect('not matching server-rendered content').toHaveBeenWarned()
  167. expect('Unknown custom element: <foo>').toHaveBeenWarned()
  168. })
  169. it('should overwrite textNodes in the correct position but with mismatching text without warning', () => {
  170. const dom = createMockSSRDOM('<div><span>foo</span></div>')
  171. new Vue({
  172. template: '<div><test></test></div>',
  173. components: {
  174. test: {
  175. data() {
  176. return { a: 'qux' }
  177. },
  178. template: '<div><span>{{a}}</span></div>'
  179. }
  180. }
  181. }).$mount(dom)
  182. expect('not matching server-rendered content').not.toHaveBeenWarned()
  183. expect(dom.querySelector('span').textContent).toBe('qux')
  184. })
  185. it('should pick up elements with no children and populate without warning', done => {
  186. const dom = createMockSSRDOM('<div><span></span></div>')
  187. const span = dom.querySelector('span')
  188. const vm = new Vue({
  189. template: '<div><test></test></div>',
  190. components: {
  191. test: {
  192. data() {
  193. return { a: 'qux' }
  194. },
  195. template: '<div><span>{{a}}</span></div>'
  196. }
  197. }
  198. }).$mount(dom)
  199. expect('not matching server-rendered content').not.toHaveBeenWarned()
  200. expect(span).toBe(vm.$el.querySelector('span'))
  201. expect(vm.$el.innerHTML).toBe('<div><span>qux</span></div>')
  202. vm.$children[0].a = 'foo'
  203. waitForUpdate(() => {
  204. expect(vm.$el.innerHTML).toBe('<div><span>foo</span></div>')
  205. }).then(done)
  206. })
  207. it('should hydrate async component', done => {
  208. const dom = createMockSSRDOM('<span>foo</span>')
  209. const span = dom.querySelector('span')
  210. const Foo = resolve =>
  211. setTimeout(() => {
  212. resolve({
  213. data: () => ({ msg: 'foo' }),
  214. template: `<span>{{ msg }}</span>`
  215. })
  216. }, 0)
  217. const vm = new Vue({
  218. template: '<div><foo ref="foo" /></div>',
  219. components: { Foo }
  220. }).$mount(dom)
  221. expect('not matching server-rendered content').not.toHaveBeenWarned()
  222. expect(dom.innerHTML).toBe('<span>foo</span>')
  223. expect(vm.$refs.foo).toBeUndefined()
  224. setTimeout(() => {
  225. expect(dom.innerHTML).toBe('<span>foo</span>')
  226. expect(vm.$refs.foo).not.toBeUndefined()
  227. vm.$refs.foo.msg = 'bar'
  228. waitForUpdate(() => {
  229. expect(dom.innerHTML).toBe('<span>bar</span>')
  230. expect(dom.querySelector('span')).toBe(span)
  231. }).then(done)
  232. }, 50)
  233. })
  234. it('should hydrate async component without showing loading', done => {
  235. const dom = createMockSSRDOM('<span>foo</span>')
  236. const span = dom.querySelector('span')
  237. const Foo = () => ({
  238. component: new Promise(resolve => {
  239. setTimeout(() => {
  240. resolve({
  241. data: () => ({ msg: 'foo' }),
  242. template: `<span>{{ msg }}</span>`
  243. })
  244. }, 10)
  245. }),
  246. delay: 1,
  247. loading: {
  248. render: h => h('span', 'loading')
  249. }
  250. })
  251. const vm = new Vue({
  252. template: '<div><foo ref="foo" /></div>',
  253. components: { Foo }
  254. }).$mount(dom)
  255. expect('not matching server-rendered content').not.toHaveBeenWarned()
  256. expect(dom.innerHTML).toBe('<span>foo</span>')
  257. expect(vm.$refs.foo).toBeUndefined()
  258. setTimeout(() => {
  259. expect(dom.innerHTML).toBe('<span>foo</span>')
  260. }, 2)
  261. setTimeout(() => {
  262. expect(dom.innerHTML).toBe('<span>foo</span>')
  263. expect(vm.$refs.foo).not.toBeUndefined()
  264. vm.$refs.foo.msg = 'bar'
  265. waitForUpdate(() => {
  266. expect(dom.innerHTML).toBe('<span>bar</span>')
  267. expect(dom.querySelector('span')).toBe(span)
  268. }).then(done)
  269. }, 50)
  270. })
  271. it('should hydrate async component by replacing DOM if error occurs', done => {
  272. const dom = createMockSSRDOM('<span>foo</span>')
  273. const Foo = () => ({
  274. component: new Promise((resolve, reject) => {
  275. setTimeout(() => {
  276. reject('something went wrong')
  277. }, 10)
  278. }),
  279. error: {
  280. render: h => h('span', 'error')
  281. }
  282. })
  283. new Vue({
  284. template: '<div><foo ref="foo" /></div>',
  285. components: { Foo }
  286. }).$mount(dom)
  287. expect('not matching server-rendered content').not.toHaveBeenWarned()
  288. expect(dom.innerHTML).toBe('<span>foo</span>')
  289. setTimeout(() => {
  290. expect('Failed to resolve async').toHaveBeenWarned()
  291. expect(dom.innerHTML).toBe('<span>error</span>')
  292. done()
  293. }, 50)
  294. })
  295. it('should hydrate v-html with children', () => {
  296. const dom = createMockSSRDOM('<span>foo</span>')
  297. new Vue({
  298. data: {
  299. html: `<span>foo</span>`
  300. },
  301. template: `<div v-html="html">hello</div>`
  302. }).$mount(dom)
  303. expect('not matching server-rendered content').not.toHaveBeenWarned()
  304. })
  305. it('should warn mismatching v-html', () => {
  306. const dom = createMockSSRDOM('<span>bar</span>')
  307. new Vue({
  308. data: {
  309. html: `<span>foo</span>`
  310. },
  311. template: `<div v-html="html">hello</div>`
  312. }).$mount(dom)
  313. expect('not matching server-rendered content').toHaveBeenWarned()
  314. })
  315. it('should hydrate with adjacent text nodes from array children (e.g. slots)', () => {
  316. const dom = createMockSSRDOM('<div>foo</div> hello')
  317. new Vue({
  318. template: `<test>hello</test>`,
  319. components: {
  320. test: {
  321. template: `
  322. <div>
  323. <div>foo</div>
  324. <slot/>
  325. </div>
  326. `
  327. }
  328. }
  329. }).$mount(dom)
  330. expect('not matching server-rendered content').not.toHaveBeenWarned()
  331. })
  332. // #7063
  333. it('should properly initialize dynamic style bindings for future updates', done => {
  334. const dom = createMockSSRDOM('<div style="padding-left:0px"></div>')
  335. const vm = new Vue({
  336. data: {
  337. style: { paddingLeft: '0px' }
  338. },
  339. template: `<div><div :style="style"></div></div>`
  340. }).$mount(dom)
  341. // should update
  342. vm.style.paddingLeft = '100px'
  343. waitForUpdate(() => {
  344. expect(dom.children[0].style.paddingLeft).toBe('100px')
  345. }).then(done)
  346. })
  347. it('should properly initialize dynamic class bindings for future updates', done => {
  348. const dom = createMockSSRDOM('<div class="foo bar"></div>')
  349. const vm = new Vue({
  350. data: {
  351. cls: [{ foo: true }, 'bar']
  352. },
  353. template: `<div><div :class="cls"></div></div>`
  354. }).$mount(dom)
  355. // should update
  356. vm.cls[0].foo = false
  357. waitForUpdate(() => {
  358. expect(dom.children[0].className).toBe('bar')
  359. }).then(done)
  360. })
  361. })