hydration.spec.ts 32 KB

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