hydration.spec.ts 32 KB

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