hydration.spec.ts 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083
  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. // #6637
  859. test('stringified root fragment', () => {
  860. mountWithHydration(`<!--[--><div></div><!--]-->`, () =>
  861. createStaticVNode(`<div></div>`, 1)
  862. )
  863. expect(`mismatch`).not.toHaveBeenWarned()
  864. })
  865. describe('mismatch handling', () => {
  866. test('text node', () => {
  867. const { container } = mountWithHydration(`foo`, () => 'bar')
  868. expect(container.textContent).toBe('bar')
  869. expect(`Hydration text mismatch`).toHaveBeenWarned()
  870. })
  871. test('element text content', () => {
  872. const { container } = mountWithHydration(`<div>foo</div>`, () =>
  873. h('div', 'bar')
  874. )
  875. expect(container.innerHTML).toBe('<div>bar</div>')
  876. expect(`Hydration text content mismatch in <div>`).toHaveBeenWarned()
  877. })
  878. test('not enough children', () => {
  879. const { container } = mountWithHydration(`<div></div>`, () =>
  880. h('div', [h('span', 'foo'), h('span', 'bar')])
  881. )
  882. expect(container.innerHTML).toBe(
  883. '<div><span>foo</span><span>bar</span></div>'
  884. )
  885. expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
  886. })
  887. test('too many children', () => {
  888. const { container } = mountWithHydration(
  889. `<div><span>foo</span><span>bar</span></div>`,
  890. () => h('div', [h('span', 'foo')])
  891. )
  892. expect(container.innerHTML).toBe('<div><span>foo</span></div>')
  893. expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
  894. })
  895. test('complete mismatch', () => {
  896. const { container } = mountWithHydration(
  897. `<div><span>foo</span><span>bar</span></div>`,
  898. () => h('div', [h('div', 'foo'), h('p', 'bar')])
  899. )
  900. expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
  901. expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
  902. })
  903. test('fragment mismatch removal', () => {
  904. const { container } = mountWithHydration(
  905. `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  906. () => h('div', [h('span', 'replaced')])
  907. )
  908. expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
  909. expect(`Hydration node mismatch`).toHaveBeenWarned()
  910. })
  911. test('fragment not enough children', () => {
  912. const { container } = mountWithHydration(
  913. `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  914. () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')])
  915. )
  916. expect(container.innerHTML).toBe(
  917. '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>'
  918. )
  919. expect(`Hydration node mismatch`).toHaveBeenWarned()
  920. })
  921. test('fragment too many children', () => {
  922. const { container } = mountWithHydration(
  923. `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  924. () => h('div', [[h('div', 'foo')], h('div', 'baz')])
  925. )
  926. expect(container.innerHTML).toBe(
  927. '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>'
  928. )
  929. // fragment ends early and attempts to hydrate the extra <div>bar</div>
  930. // as 2nd fragment child.
  931. expect(`Hydration text content mismatch`).toHaveBeenWarned()
  932. // excessive children removal
  933. expect(`Hydration children mismatch`).toHaveBeenWarned()
  934. })
  935. test('Teleport target has empty children', () => {
  936. const teleportContainer = document.createElement('div')
  937. teleportContainer.id = 'teleport'
  938. document.body.appendChild(teleportContainer)
  939. mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
  940. h(Teleport, { to: '#teleport' }, [h('span', 'value')])
  941. )
  942. expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
  943. expect(`Hydration children mismatch`).toHaveBeenWarned()
  944. })
  945. })
  946. })