hydration.spec.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053
  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('<!--[--><!--]-->', () =>
  807. h(Comp)
  808. )
  809. expect(container.childNodes.length).toBe(3)
  810. const text = container.childNodes[1]
  811. expect(text.nodeType).toBe(3)
  812. expect(vnode.el).toBe(container.childNodes[0])
  813. // component => slot fragment => text node
  814. expect((vnode as any).component?.subTree.children[0].el).toBe(text)
  815. })
  816. test('app.unmount()', async () => {
  817. const container = document.createElement('DIV')
  818. container.innerHTML = '<button></button>'
  819. const App = defineComponent({
  820. setup(_, { expose }) {
  821. const count = ref(0)
  822. expose({ count })
  823. return () =>
  824. h('button', {
  825. onClick: () => count.value++
  826. })
  827. }
  828. })
  829. const app = createSSRApp(App)
  830. const vm = app.mount(container)
  831. await nextTick()
  832. expect((container as any)._vnode).toBeDefined()
  833. // @ts-expect-error - expose()'d properties are not available on vm type
  834. expect(vm.count).toBe(0)
  835. app.unmount()
  836. expect((container as any)._vnode).toBe(null)
  837. })
  838. describe('mismatch handling', () => {
  839. test('text node', () => {
  840. const { container } = mountWithHydration(`foo`, () => 'bar')
  841. expect(container.textContent).toBe('bar')
  842. expect(`Hydration text mismatch`).toHaveBeenWarned()
  843. })
  844. test('element text content', () => {
  845. const { container } = mountWithHydration(`<div>foo</div>`, () =>
  846. h('div', 'bar')
  847. )
  848. expect(container.innerHTML).toBe('<div>bar</div>')
  849. expect(`Hydration text content mismatch in <div>`).toHaveBeenWarned()
  850. })
  851. test('not enough children', () => {
  852. const { container } = mountWithHydration(`<div></div>`, () =>
  853. h('div', [h('span', 'foo'), h('span', 'bar')])
  854. )
  855. expect(container.innerHTML).toBe(
  856. '<div><span>foo</span><span>bar</span></div>'
  857. )
  858. expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
  859. })
  860. test('too many children', () => {
  861. const { container } = mountWithHydration(
  862. `<div><span>foo</span><span>bar</span></div>`,
  863. () => h('div', [h('span', 'foo')])
  864. )
  865. expect(container.innerHTML).toBe('<div><span>foo</span></div>')
  866. expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
  867. })
  868. test('complete mismatch', () => {
  869. const { container } = mountWithHydration(
  870. `<div><span>foo</span><span>bar</span></div>`,
  871. () => h('div', [h('div', 'foo'), h('p', 'bar')])
  872. )
  873. expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
  874. expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
  875. })
  876. test('fragment mismatch removal', () => {
  877. const { container } = mountWithHydration(
  878. `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  879. () => h('div', [h('span', 'replaced')])
  880. )
  881. expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
  882. expect(`Hydration node mismatch`).toHaveBeenWarned()
  883. })
  884. test('fragment not enough children', () => {
  885. const { container } = mountWithHydration(
  886. `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  887. () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')])
  888. )
  889. expect(container.innerHTML).toBe(
  890. '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>'
  891. )
  892. expect(`Hydration node mismatch`).toHaveBeenWarned()
  893. })
  894. test('fragment too many children', () => {
  895. const { container } = mountWithHydration(
  896. `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  897. () => h('div', [[h('div', 'foo')], h('div', 'baz')])
  898. )
  899. expect(container.innerHTML).toBe(
  900. '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>'
  901. )
  902. // fragment ends early and attempts to hydrate the extra <div>bar</div>
  903. // as 2nd fragment child.
  904. expect(`Hydration text content mismatch`).toHaveBeenWarned()
  905. // excessive children removal
  906. expect(`Hydration children mismatch`).toHaveBeenWarned()
  907. })
  908. test('Teleport target has empty children', () => {
  909. const teleportContainer = document.createElement('div')
  910. teleportContainer.id = 'teleport'
  911. document.body.appendChild(teleportContainer)
  912. mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
  913. h(Teleport, { to: '#teleport' }, [h('span', 'value')])
  914. )
  915. expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
  916. expect(`Hydration children mismatch`).toHaveBeenWarned()
  917. })
  918. })
  919. })