hydration.spec.ts 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024
  1. import {
  2. createSSRApp,
  3. h,
  4. ref,
  5. nextTick,
  6. VNode,
  7. Teleport,
  8. createStaticVNode,
  9. Suspense,
  10. onMounted,
  11. defineAsyncComponent,
  12. defineComponent,
  13. createTextVNode,
  14. createVNode,
  15. withDirectives,
  16. vModelCheckbox,
  17. renderSlot
  18. } from '@vue/runtime-dom'
  19. import { renderToString, SSRContext } from '@vue/server-renderer'
  20. import { PatchFlags } from '../../shared/src'
  21. function mountWithHydration(html: string, render: () => any) {
  22. const container = document.createElement('div')
  23. container.innerHTML = html
  24. const app = createSSRApp({
  25. render
  26. })
  27. return {
  28. vnode: app.mount(container).$.subTree as VNode<Node, Element> & {
  29. el: Element
  30. },
  31. container
  32. }
  33. }
  34. const triggerEvent = (type: string, el: Element) => {
  35. const event = new Event(type)
  36. el.dispatchEvent(event)
  37. }
  38. describe('SSR hydration', () => {
  39. beforeEach(() => {
  40. document.body.innerHTML = ''
  41. })
  42. test('text', async () => {
  43. const msg = ref('foo')
  44. const { vnode, container } = mountWithHydration('foo', () => msg.value)
  45. expect(vnode.el).toBe(container.firstChild)
  46. expect(container.textContent).toBe('foo')
  47. msg.value = 'bar'
  48. await nextTick()
  49. expect(container.textContent).toBe('bar')
  50. })
  51. test('empty text', async () => {
  52. const { container } = mountWithHydration('<div></div>', () =>
  53. h('div', createTextVNode(''))
  54. )
  55. expect(container.textContent).toBe('')
  56. expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
  57. })
  58. test('comment', () => {
  59. const { vnode, container } = mountWithHydration('<!---->', () => null)
  60. expect(vnode.el).toBe(container.firstChild)
  61. expect(vnode.el.nodeType).toBe(8) // comment
  62. })
  63. test('static', () => {
  64. const html = '<div><span>hello</span></div>'
  65. const { vnode, container } = mountWithHydration(html, () =>
  66. createStaticVNode('', 1)
  67. )
  68. expect(vnode.el).toBe(container.firstChild)
  69. expect(vnode.el.outerHTML).toBe(html)
  70. expect(vnode.anchor).toBe(container.firstChild)
  71. expect(vnode.children).toBe(html)
  72. })
  73. test('static (multiple elements)', () => {
  74. const staticContent = '<div></div><span>hello</span>'
  75. const html = `<div><div>hi</div>` + staticContent + `<div>ho</div></div>`
  76. const n1 = h('div', 'hi')
  77. const s = createStaticVNode('', 2)
  78. const n2 = h('div', 'ho')
  79. const { container } = mountWithHydration(html, () => h('div', [n1, s, n2]))
  80. const div = container.firstChild!
  81. expect(n1.el).toBe(div.firstChild)
  82. expect(n2.el).toBe(div.lastChild)
  83. expect(s.el).toBe(div.childNodes[1])
  84. expect(s.anchor).toBe(div.childNodes[2])
  85. expect(s.children).toBe(staticContent)
  86. })
  87. test('element with text children', async () => {
  88. const msg = ref('foo')
  89. const { vnode, container } = mountWithHydration(
  90. '<div class="foo">foo</div>',
  91. () => h('div', { class: msg.value }, msg.value)
  92. )
  93. expect(vnode.el).toBe(container.firstChild)
  94. expect(container.firstChild!.textContent).toBe('foo')
  95. msg.value = 'bar'
  96. await nextTick()
  97. expect(container.innerHTML).toBe(`<div class="bar">bar</div>`)
  98. })
  99. test('element with elements children', async () => {
  100. const msg = ref('foo')
  101. const fn = jest.fn()
  102. const { vnode, container } = mountWithHydration(
  103. '<div><span>foo</span><span class="foo"></span></div>',
  104. () =>
  105. h('div', [
  106. h('span', msg.value),
  107. h('span', { class: msg.value, onClick: fn })
  108. ])
  109. )
  110. expect(vnode.el).toBe(container.firstChild)
  111. expect((vnode.children as VNode[])[0].el).toBe(
  112. container.firstChild!.childNodes[0]
  113. )
  114. expect((vnode.children as VNode[])[1].el).toBe(
  115. container.firstChild!.childNodes[1]
  116. )
  117. // event handler
  118. triggerEvent('click', vnode.el.querySelector('.foo')!)
  119. expect(fn).toHaveBeenCalled()
  120. msg.value = 'bar'
  121. await nextTick()
  122. expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
  123. })
  124. test('element with ref', () => {
  125. const el = ref()
  126. const { vnode, container } = mountWithHydration('<div></div>', () =>
  127. h('div', { ref: el })
  128. )
  129. expect(vnode.el).toBe(container.firstChild)
  130. expect(el.value).toBe(vnode.el)
  131. })
  132. test('Fragment', async () => {
  133. const msg = ref('foo')
  134. const fn = jest.fn()
  135. const { vnode, container } = mountWithHydration(
  136. '<div><!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]--></div>',
  137. () =>
  138. h('div', [
  139. [h('span', msg.value), [h('span', { class: msg.value, onClick: fn })]]
  140. ])
  141. )
  142. expect(vnode.el).toBe(container.firstChild)
  143. expect(vnode.el.innerHTML).toBe(
  144. `<!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]-->`
  145. )
  146. // start fragment 1
  147. const fragment1 = (vnode.children as VNode[])[0]
  148. expect(fragment1.el).toBe(vnode.el.childNodes[0])
  149. const fragment1Children = fragment1.children as VNode[]
  150. // first <span>
  151. expect(fragment1Children[0].el!.tagName).toBe('SPAN')
  152. expect(fragment1Children[0].el).toBe(vnode.el.childNodes[1])
  153. // start fragment 2
  154. const fragment2 = fragment1Children[1]
  155. expect(fragment2.el).toBe(vnode.el.childNodes[2])
  156. const fragment2Children = fragment2.children as VNode[]
  157. // second <span>
  158. expect(fragment2Children[0].el!.tagName).toBe('SPAN')
  159. expect(fragment2Children[0].el).toBe(vnode.el.childNodes[3])
  160. // end fragment 2
  161. expect(fragment2.anchor).toBe(vnode.el.childNodes[4])
  162. // end fragment 1
  163. expect(fragment1.anchor).toBe(vnode.el.childNodes[5])
  164. // event handler
  165. triggerEvent('click', vnode.el.querySelector('.foo')!)
  166. expect(fn).toHaveBeenCalled()
  167. msg.value = 'bar'
  168. await nextTick()
  169. expect(vnode.el.innerHTML).toBe(
  170. `<!--[--><span>bar</span><!--[--><span class="bar"></span><!--]--><!--]-->`
  171. )
  172. })
  173. test('Teleport', async () => {
  174. const msg = ref('foo')
  175. const fn = jest.fn()
  176. const teleportContainer = document.createElement('div')
  177. teleportContainer.id = 'teleport'
  178. teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!--teleport anchor-->`
  179. document.body.appendChild(teleportContainer)
  180. const { vnode, container } = mountWithHydration(
  181. '<!--teleport start--><!--teleport end-->',
  182. () =>
  183. h(Teleport, { to: '#teleport' }, [
  184. h('span', msg.value),
  185. h('span', { class: msg.value, onClick: fn })
  186. ])
  187. )
  188. expect(vnode.el).toBe(container.firstChild)
  189. expect(vnode.anchor).toBe(container.lastChild)
  190. expect(vnode.target).toBe(teleportContainer)
  191. expect((vnode.children as VNode[])[0].el).toBe(
  192. teleportContainer.childNodes[0]
  193. )
  194. expect((vnode.children as VNode[])[1].el).toBe(
  195. teleportContainer.childNodes[1]
  196. )
  197. expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2])
  198. // event handler
  199. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  200. expect(fn).toHaveBeenCalled()
  201. msg.value = 'bar'
  202. await nextTick()
  203. expect(teleportContainer.innerHTML).toBe(
  204. `<span>bar</span><span class="bar"></span><!--teleport anchor-->`
  205. )
  206. })
  207. test('Teleport (multiple + integration)', async () => {
  208. const msg = ref('foo')
  209. const fn1 = jest.fn()
  210. const fn2 = jest.fn()
  211. const Comp = () => [
  212. h(Teleport, { to: '#teleport2' }, [
  213. h('span', msg.value),
  214. h('span', { class: msg.value, onClick: fn1 })
  215. ]),
  216. h(Teleport, { to: '#teleport2' }, [
  217. h('span', msg.value + '2'),
  218. h('span', { class: msg.value + '2', onClick: fn2 })
  219. ])
  220. ]
  221. const teleportContainer = document.createElement('div')
  222. teleportContainer.id = 'teleport2'
  223. const ctx: SSRContext = {}
  224. const mainHtml = await renderToString(h(Comp), ctx)
  225. expect(mainHtml).toMatchInlineSnapshot(
  226. `"<!--[--><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--><!--]-->"`
  227. )
  228. const teleportHtml = ctx.teleports!['#teleport2']
  229. expect(teleportHtml).toMatchInlineSnapshot(
  230. `"<span>foo</span><span class=\\"foo\\"></span><!--teleport anchor--><span>foo2</span><span class=\\"foo2\\"></span><!--teleport anchor-->"`
  231. )
  232. teleportContainer.innerHTML = teleportHtml
  233. document.body.appendChild(teleportContainer)
  234. const { vnode, container } = mountWithHydration(mainHtml, Comp)
  235. expect(vnode.el).toBe(container.firstChild)
  236. const teleportVnode1 = (vnode.children as VNode[])[0]
  237. const teleportVnode2 = (vnode.children as VNode[])[1]
  238. expect(teleportVnode1.el).toBe(container.childNodes[1])
  239. expect(teleportVnode1.anchor).toBe(container.childNodes[2])
  240. expect(teleportVnode2.el).toBe(container.childNodes[3])
  241. expect(teleportVnode2.anchor).toBe(container.childNodes[4])
  242. expect(teleportVnode1.target).toBe(teleportContainer)
  243. expect((teleportVnode1 as any).children[0].el).toBe(
  244. teleportContainer.childNodes[0]
  245. )
  246. expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[2])
  247. expect(teleportVnode2.target).toBe(teleportContainer)
  248. expect((teleportVnode2 as any).children[0].el).toBe(
  249. teleportContainer.childNodes[3]
  250. )
  251. expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[5])
  252. // // event handler
  253. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  254. expect(fn1).toHaveBeenCalled()
  255. triggerEvent('click', teleportContainer.querySelector('.foo2')!)
  256. expect(fn2).toHaveBeenCalled()
  257. msg.value = 'bar'
  258. await nextTick()
  259. expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
  260. `"<span>bar</span><span class=\\"bar\\"></span><!--teleport anchor--><span>bar2</span><span class=\\"bar2\\"></span><!--teleport anchor-->"`
  261. )
  262. })
  263. test('Teleport (disabled)', async () => {
  264. const msg = ref('foo')
  265. const fn1 = jest.fn()
  266. const fn2 = jest.fn()
  267. const Comp = () => [
  268. h('div', 'foo'),
  269. h(Teleport, { to: '#teleport3', disabled: true }, [
  270. h('span', msg.value),
  271. h('span', { class: msg.value, onClick: fn1 })
  272. ]),
  273. h('div', { class: msg.value + '2', onClick: fn2 }, 'bar')
  274. ]
  275. const teleportContainer = document.createElement('div')
  276. teleportContainer.id = 'teleport3'
  277. const ctx: SSRContext = {}
  278. const mainHtml = await renderToString(h(Comp), ctx)
  279. expect(mainHtml).toMatchInlineSnapshot(
  280. `"<!--[--><div>foo</div><!--teleport start--><span>foo</span><span class=\\"foo\\"></span><!--teleport end--><div class=\\"foo2\\">bar</div><!--]-->"`
  281. )
  282. const teleportHtml = ctx.teleports!['#teleport3']
  283. expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)
  284. teleportContainer.innerHTML = teleportHtml
  285. document.body.appendChild(teleportContainer)
  286. const { vnode, container } = mountWithHydration(mainHtml, Comp)
  287. expect(vnode.el).toBe(container.firstChild)
  288. const children = vnode.children as VNode[]
  289. expect(children[0].el).toBe(container.childNodes[1])
  290. const teleportVnode = children[1]
  291. expect(teleportVnode.el).toBe(container.childNodes[2])
  292. expect((teleportVnode.children as VNode[])[0].el).toBe(
  293. container.childNodes[3]
  294. )
  295. expect((teleportVnode.children as VNode[])[1].el).toBe(
  296. container.childNodes[4]
  297. )
  298. expect(teleportVnode.anchor).toBe(container.childNodes[5])
  299. expect(children[2].el).toBe(container.childNodes[6])
  300. expect(teleportVnode.target).toBe(teleportContainer)
  301. expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[0])
  302. // // event handler
  303. triggerEvent('click', container.querySelector('.foo')!)
  304. expect(fn1).toHaveBeenCalled()
  305. triggerEvent('click', container.querySelector('.foo2')!)
  306. expect(fn2).toHaveBeenCalled()
  307. msg.value = 'bar'
  308. await nextTick()
  309. expect(container.innerHTML).toMatchInlineSnapshot(
  310. `"<!--[--><div>foo</div><!--teleport start--><span>bar</span><span class=\\"bar\\"></span><!--teleport end--><div class=\\"bar2\\">bar</div><!--]-->"`
  311. )
  312. })
  313. test('Teleport (as component root)', () => {
  314. const teleportContainer = document.createElement('div')
  315. teleportContainer.id = 'teleport4'
  316. teleportContainer.innerHTML = `hello<!--teleport anchor-->`
  317. document.body.appendChild(teleportContainer)
  318. const wrapper = {
  319. render() {
  320. return h(Teleport, { to: '#teleport4' }, ['hello'])
  321. }
  322. }
  323. const { vnode, container } = mountWithHydration(
  324. '<div><!--teleport start--><!--teleport end--><div></div></div>',
  325. () => h('div', [h(wrapper), h('div')])
  326. )
  327. expect(vnode.el).toBe(container.firstChild)
  328. // component el
  329. const wrapperVNode = (vnode as any).children[0]
  330. const tpStart = container.firstChild?.firstChild
  331. const tpEnd = tpStart?.nextSibling
  332. expect(wrapperVNode.el).toBe(tpStart)
  333. expect(wrapperVNode.component.subTree.el).toBe(tpStart)
  334. expect(wrapperVNode.component.subTree.anchor).toBe(tpEnd)
  335. // next node hydrate properly
  336. const nextVNode = (vnode as any).children[1]
  337. expect(nextVNode.el).toBe(container.firstChild?.lastChild)
  338. })
  339. test('Teleport (nested)', () => {
  340. const teleportContainer = document.createElement('div')
  341. teleportContainer.id = 'teleport5'
  342. teleportContainer.innerHTML = `<div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><div>child</div><!--teleport anchor-->`
  343. document.body.appendChild(teleportContainer)
  344. const { vnode, container } = mountWithHydration(
  345. '<!--teleport start--><!--teleport end-->',
  346. () =>
  347. h(Teleport, { to: '#teleport5' }, [
  348. h('div', [h(Teleport, { to: '#teleport5' }, [h('div', 'child')])])
  349. ])
  350. )
  351. expect(vnode.el).toBe(container.firstChild)
  352. expect(vnode.anchor).toBe(container.lastChild)
  353. const childDivVNode = (vnode as any).children[0]
  354. const div = teleportContainer.firstChild
  355. expect(childDivVNode.el).toBe(div)
  356. expect(vnode.targetAnchor).toBe(div?.nextSibling)
  357. const childTeleportVNode = childDivVNode.children[0]
  358. expect(childTeleportVNode.el).toBe(div?.firstChild)
  359. expect(childTeleportVNode.anchor).toBe(div?.lastChild)
  360. expect(childTeleportVNode.targetAnchor).toBe(teleportContainer.lastChild)
  361. expect(childTeleportVNode.children[0].el).toBe(
  362. teleportContainer.lastChild?.previousSibling
  363. )
  364. })
  365. // compile SSR + client render fn from the same template & hydrate
  366. test('full compiler integration', async () => {
  367. const mounted: string[] = []
  368. const log = jest.fn()
  369. const toggle = ref(true)
  370. const Child = {
  371. data() {
  372. return {
  373. count: 0,
  374. text: 'hello',
  375. style: {
  376. color: 'red'
  377. }
  378. }
  379. },
  380. mounted() {
  381. mounted.push('child')
  382. },
  383. template: `
  384. <div>
  385. <span class="count" :style="style">{{ count }}</span>
  386. <button class="inc" @click="count++">inc</button>
  387. <button class="change" @click="style.color = 'green'" >change color</button>
  388. <button class="emit" @click="$emit('foo')">emit</button>
  389. <span class="text">{{ text }}</span>
  390. <input v-model="text">
  391. </div>
  392. `
  393. }
  394. const App = {
  395. setup() {
  396. return { toggle }
  397. },
  398. mounted() {
  399. mounted.push('parent')
  400. },
  401. template: `
  402. <div>
  403. <span>hello</span>
  404. <template v-if="toggle">
  405. <Child @foo="log('child')"/>
  406. <template v-if="true">
  407. <button class="parent-click" @click="log('click')">click me</button>
  408. </template>
  409. </template>
  410. <span>hello</span>
  411. </div>`,
  412. components: {
  413. Child
  414. },
  415. methods: {
  416. log
  417. }
  418. }
  419. const container = document.createElement('div')
  420. // server render
  421. container.innerHTML = await renderToString(h(App))
  422. // hydrate
  423. createSSRApp(App).mount(container)
  424. // assert interactions
  425. // 1. parent button click
  426. triggerEvent('click', container.querySelector('.parent-click')!)
  427. expect(log).toHaveBeenCalledWith('click')
  428. // 2. child inc click + text interpolation
  429. const count = container.querySelector('.count') as HTMLElement
  430. expect(count.textContent).toBe(`0`)
  431. triggerEvent('click', container.querySelector('.inc')!)
  432. await nextTick()
  433. expect(count.textContent).toBe(`1`)
  434. // 3. child color click + style binding
  435. expect(count.style.color).toBe('red')
  436. triggerEvent('click', container.querySelector('.change')!)
  437. await nextTick()
  438. expect(count.style.color).toBe('green')
  439. // 4. child event emit
  440. triggerEvent('click', container.querySelector('.emit')!)
  441. expect(log).toHaveBeenCalledWith('child')
  442. // 5. child v-model
  443. const text = container.querySelector('.text')!
  444. const input = container.querySelector('input')!
  445. expect(text.textContent).toBe('hello')
  446. input.value = 'bye'
  447. triggerEvent('input', input)
  448. await nextTick()
  449. expect(text.textContent).toBe('bye')
  450. })
  451. test('handle click error in ssr mode', async () => {
  452. const App = {
  453. setup() {
  454. const throwError = () => {
  455. throw new Error('Sentry Error')
  456. }
  457. return { throwError }
  458. },
  459. template: `
  460. <div>
  461. <button class="parent-click" @click="throwError">click me</button>
  462. </div>`
  463. }
  464. const container = document.createElement('div')
  465. // server render
  466. container.innerHTML = await renderToString(h(App))
  467. // hydrate
  468. const app = createSSRApp(App)
  469. const handler = (app.config.errorHandler = jest.fn())
  470. app.mount(container)
  471. // assert interactions
  472. // parent button click
  473. triggerEvent('click', container.querySelector('.parent-click')!)
  474. expect(handler).toHaveBeenCalled()
  475. })
  476. test('handle blur error in ssr mode', async () => {
  477. const App = {
  478. setup() {
  479. const throwError = () => {
  480. throw new Error('Sentry Error')
  481. }
  482. return { throwError }
  483. },
  484. template: `
  485. <div>
  486. <input class="parent-click" @blur="throwError"/>
  487. </div>`
  488. }
  489. const container = document.createElement('div')
  490. // server render
  491. container.innerHTML = await renderToString(h(App))
  492. // hydrate
  493. const app = createSSRApp(App)
  494. const handler = (app.config.errorHandler = jest.fn())
  495. app.mount(container)
  496. // assert interactions
  497. // parent blur event
  498. triggerEvent('blur', container.querySelector('.parent-click')!)
  499. expect(handler).toHaveBeenCalled()
  500. })
  501. test('Suspense', async () => {
  502. const AsyncChild = {
  503. async setup() {
  504. const count = ref(0)
  505. return () =>
  506. h(
  507. 'span',
  508. {
  509. onClick: () => {
  510. count.value++
  511. }
  512. },
  513. count.value
  514. )
  515. }
  516. }
  517. const { vnode, container } = mountWithHydration('<span>0</span>', () =>
  518. h(Suspense, () => h(AsyncChild))
  519. )
  520. expect(vnode.el).toBe(container.firstChild)
  521. // wait for hydration to finish
  522. await new Promise(r => setTimeout(r))
  523. triggerEvent('click', container.querySelector('span')!)
  524. await nextTick()
  525. expect(container.innerHTML).toBe(`<span>1</span>`)
  526. })
  527. test('Suspense (full integration)', async () => {
  528. const mountedCalls: number[] = []
  529. const asyncDeps: Promise<any>[] = []
  530. const AsyncChild = defineComponent({
  531. props: ['n'],
  532. async setup(props) {
  533. const count = ref(props.n)
  534. onMounted(() => {
  535. mountedCalls.push(props.n)
  536. })
  537. const p = new Promise(r => setTimeout(r, props.n * 10))
  538. asyncDeps.push(p)
  539. await p
  540. return () =>
  541. h(
  542. 'span',
  543. {
  544. onClick: () => {
  545. count.value++
  546. }
  547. },
  548. count.value
  549. )
  550. }
  551. })
  552. const done = jest.fn()
  553. const App = {
  554. template: `
  555. <Suspense @resolve="done">
  556. <div>
  557. <AsyncChild :n="1" />
  558. <AsyncChild :n="2" />
  559. </div>
  560. </Suspense>`,
  561. components: {
  562. AsyncChild
  563. },
  564. methods: {
  565. done
  566. }
  567. }
  568. const container = document.createElement('div')
  569. // server render
  570. container.innerHTML = await renderToString(h(App))
  571. expect(container.innerHTML).toMatchInlineSnapshot(
  572. `"<div><span>1</span><span>2</span></div>"`
  573. )
  574. // reset asyncDeps from ssr
  575. asyncDeps.length = 0
  576. // hydrate
  577. createSSRApp(App).mount(container)
  578. expect(mountedCalls.length).toBe(0)
  579. expect(asyncDeps.length).toBe(2)
  580. // wait for hydration to complete
  581. await Promise.all(asyncDeps)
  582. await new Promise(r => setTimeout(r))
  583. // should flush buffered effects
  584. expect(mountedCalls).toMatchObject([1, 2])
  585. expect(container.innerHTML).toMatch(
  586. `<div><span>1</span><span>2</span></div>`
  587. )
  588. const span1 = container.querySelector('span')!
  589. triggerEvent('click', span1)
  590. await nextTick()
  591. expect(container.innerHTML).toMatch(
  592. `<div><span>2</span><span>2</span></div>`
  593. )
  594. const span2 = span1.nextSibling as Element
  595. triggerEvent('click', span2)
  596. await nextTick()
  597. expect(container.innerHTML).toMatch(
  598. `<div><span>2</span><span>3</span></div>`
  599. )
  600. })
  601. test('async component', async () => {
  602. const spy = jest.fn()
  603. const Comp = () =>
  604. h(
  605. 'button',
  606. {
  607. onClick: spy
  608. },
  609. 'hello!'
  610. )
  611. let serverResolve: any
  612. let AsyncComp = defineAsyncComponent(
  613. () =>
  614. new Promise(r => {
  615. serverResolve = r
  616. })
  617. )
  618. const App = {
  619. render() {
  620. return ['hello', h(AsyncComp), 'world']
  621. }
  622. }
  623. // server render
  624. const htmlPromise = renderToString(h(App))
  625. serverResolve(Comp)
  626. const html = await htmlPromise
  627. expect(html).toMatchInlineSnapshot(
  628. `"<!--[-->hello<button>hello!</button>world<!--]-->"`
  629. )
  630. // hydration
  631. let clientResolve: any
  632. AsyncComp = defineAsyncComponent(
  633. () =>
  634. new Promise(r => {
  635. clientResolve = r
  636. })
  637. )
  638. const container = document.createElement('div')
  639. container.innerHTML = html
  640. createSSRApp(App).mount(container)
  641. // hydration not complete yet
  642. triggerEvent('click', container.querySelector('button')!)
  643. expect(spy).not.toHaveBeenCalled()
  644. // resolve
  645. clientResolve(Comp)
  646. await new Promise(r => setTimeout(r))
  647. // should be hydrated now
  648. triggerEvent('click', container.querySelector('button')!)
  649. expect(spy).toHaveBeenCalled()
  650. })
  651. test('update async wrapper before resolve', async () => {
  652. const Comp = {
  653. render() {
  654. return h('h1', 'Async component')
  655. }
  656. }
  657. let serverResolve: any
  658. let AsyncComp = defineAsyncComponent(
  659. () =>
  660. new Promise(r => {
  661. serverResolve = r
  662. })
  663. )
  664. const bol = ref(true)
  665. const App = {
  666. setup() {
  667. onMounted(() => {
  668. // change state, this makes updateComponent(AsyncComp) execute before
  669. // the async component is resolved
  670. bol.value = false
  671. })
  672. return () => {
  673. return [bol.value ? 'hello' : 'world', h(AsyncComp)]
  674. }
  675. }
  676. }
  677. // server render
  678. const htmlPromise = renderToString(h(App))
  679. serverResolve(Comp)
  680. const html = await htmlPromise
  681. expect(html).toMatchInlineSnapshot(
  682. `"<!--[-->hello<h1>Async component</h1><!--]-->"`
  683. )
  684. // hydration
  685. let clientResolve: any
  686. AsyncComp = defineAsyncComponent(
  687. () =>
  688. new Promise(r => {
  689. clientResolve = r
  690. })
  691. )
  692. const container = document.createElement('div')
  693. container.innerHTML = html
  694. createSSRApp(App).mount(container)
  695. // resolve
  696. clientResolve(Comp)
  697. await new Promise(r => setTimeout(r))
  698. // should be hydrated now
  699. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  700. expect(container.innerHTML).toMatchInlineSnapshot(
  701. `"<!--[-->world<h1>Async component</h1><!--]-->"`
  702. )
  703. })
  704. // #3787
  705. test('unmount async wrapper before load', async () => {
  706. let resolve: any
  707. const AsyncComp = defineAsyncComponent(
  708. () =>
  709. new Promise(r => {
  710. resolve = r
  711. })
  712. )
  713. const show = ref(true)
  714. const root = document.createElement('div')
  715. root.innerHTML = '<div><div>async</div></div>'
  716. createSSRApp({
  717. render() {
  718. return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
  719. }
  720. }).mount(root)
  721. show.value = false
  722. await nextTick()
  723. expect(root.innerHTML).toBe('<div><div>hi</div></div>')
  724. resolve({})
  725. })
  726. test('unmount async wrapper before load (fragment)', async () => {
  727. let resolve: any
  728. const AsyncComp = defineAsyncComponent(
  729. () =>
  730. new Promise(r => {
  731. resolve = r
  732. })
  733. )
  734. const show = ref(true)
  735. const root = document.createElement('div')
  736. root.innerHTML = '<div><!--[-->async<!--]--></div>'
  737. createSSRApp({
  738. render() {
  739. return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
  740. }
  741. }).mount(root)
  742. show.value = false
  743. await nextTick()
  744. expect(root.innerHTML).toBe('<div><div>hi</div></div>')
  745. resolve({})
  746. })
  747. test('elements with camel-case in svg ', () => {
  748. const { vnode, container } = mountWithHydration(
  749. '<animateTransform></animateTransform>',
  750. () => h('animateTransform')
  751. )
  752. expect(vnode.el).toBe(container.firstChild)
  753. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  754. })
  755. test('SVG as a mount container', () => {
  756. const svgContainer = document.createElement('svg')
  757. svgContainer.innerHTML = '<g></g>'
  758. const app = createSSRApp({
  759. render: () => h('g')
  760. })
  761. expect(
  762. (
  763. app.mount(svgContainer).$.subTree as VNode<Node, Element> & {
  764. el: Element
  765. }
  766. ).el instanceof SVGElement
  767. )
  768. })
  769. test('force hydrate input v-model with non-string value bindings', () => {
  770. const { container } = mountWithHydration(
  771. '<input type="checkbox" value="true">',
  772. () =>
  773. withDirectives(
  774. createVNode(
  775. 'input',
  776. { type: 'checkbox', 'true-value': true },
  777. null,
  778. PatchFlags.PROPS,
  779. ['true-value']
  780. ),
  781. [[vModelCheckbox, true]]
  782. )
  783. )
  784. expect((container.firstChild as any)._trueValue).toBe(true)
  785. })
  786. test('force hydrate select option with non-string value bindings', () => {
  787. const { container } = mountWithHydration(
  788. '<select><option :value="true">ok</option></select>',
  789. () =>
  790. h('select', [
  791. // hoisted because bound value is a constant...
  792. createVNode('option', { value: true }, null, -1 /* HOISTED */)
  793. ])
  794. )
  795. expect((container.firstChild!.firstChild as any)._value).toBe(true)
  796. })
  797. // #5728
  798. test('empty text node in slot', () => {
  799. const Comp = {
  800. render(this: any) {
  801. return renderSlot(this.$slots, 'default', {}, () => [
  802. createTextVNode('')
  803. ])
  804. }
  805. }
  806. const { container, vnode } = mountWithHydration('<!--[--><!--]-->', () => h(Comp))
  807. expect(container.childNodes.length).toBe(3)
  808. const text = container.childNodes[1]
  809. expect(text.nodeType).toBe(3)
  810. expect(vnode.el).toBe(container.childNodes[0])
  811. // component => slot fragment => text node
  812. expect((vnode as any).component?.subTree.children[0].el).toBe(text)
  813. })
  814. describe('mismatch handling', () => {
  815. test('text node', () => {
  816. const { container } = mountWithHydration(`foo`, () => 'bar')
  817. expect(container.textContent).toBe('bar')
  818. expect(`Hydration text mismatch`).toHaveBeenWarned()
  819. })
  820. test('element text content', () => {
  821. const { container } = mountWithHydration(`<div>foo</div>`, () =>
  822. h('div', 'bar')
  823. )
  824. expect(container.innerHTML).toBe('<div>bar</div>')
  825. expect(`Hydration text content mismatch in <div>`).toHaveBeenWarned()
  826. })
  827. test('not enough children', () => {
  828. const { container } = mountWithHydration(`<div></div>`, () =>
  829. h('div', [h('span', 'foo'), h('span', 'bar')])
  830. )
  831. expect(container.innerHTML).toBe(
  832. '<div><span>foo</span><span>bar</span></div>'
  833. )
  834. expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
  835. })
  836. test('too many children', () => {
  837. const { container } = mountWithHydration(
  838. `<div><span>foo</span><span>bar</span></div>`,
  839. () => h('div', [h('span', 'foo')])
  840. )
  841. expect(container.innerHTML).toBe('<div><span>foo</span></div>')
  842. expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
  843. })
  844. test('complete mismatch', () => {
  845. const { container } = mountWithHydration(
  846. `<div><span>foo</span><span>bar</span></div>`,
  847. () => h('div', [h('div', 'foo'), h('p', 'bar')])
  848. )
  849. expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
  850. expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
  851. })
  852. test('fragment mismatch removal', () => {
  853. const { container } = mountWithHydration(
  854. `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  855. () => h('div', [h('span', 'replaced')])
  856. )
  857. expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
  858. expect(`Hydration node mismatch`).toHaveBeenWarned()
  859. })
  860. test('fragment not enough children', () => {
  861. const { container } = mountWithHydration(
  862. `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  863. () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')])
  864. )
  865. expect(container.innerHTML).toBe(
  866. '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>'
  867. )
  868. expect(`Hydration node mismatch`).toHaveBeenWarned()
  869. })
  870. test('fragment too many children', () => {
  871. const { container } = mountWithHydration(
  872. `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  873. () => h('div', [[h('div', 'foo')], h('div', 'baz')])
  874. )
  875. expect(container.innerHTML).toBe(
  876. '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>'
  877. )
  878. // fragment ends early and attempts to hydrate the extra <div>bar</div>
  879. // as 2nd fragment child.
  880. expect(`Hydration text content mismatch`).toHaveBeenWarned()
  881. // excessive children removal
  882. expect(`Hydration children mismatch`).toHaveBeenWarned()
  883. })
  884. test('Teleport target has empty children', () => {
  885. const teleportContainer = document.createElement('div')
  886. teleportContainer.id = 'teleport'
  887. document.body.appendChild(teleportContainer)
  888. mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
  889. h(Teleport, { to: '#teleport' }, [h('span', 'value')])
  890. )
  891. expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
  892. expect(`Hydration children mismatch`).toHaveBeenWarned()
  893. })
  894. })
  895. })