hydration.spec.ts 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463
  1. /**
  2. * @vitest-environment jsdom
  3. */
  4. import {
  5. Suspense,
  6. Teleport,
  7. Transition,
  8. type VNode,
  9. createCommentVNode,
  10. createSSRApp,
  11. createStaticVNode,
  12. createTextVNode,
  13. createVNode,
  14. defineAsyncComponent,
  15. defineComponent,
  16. h,
  17. nextTick,
  18. onMounted,
  19. ref,
  20. renderSlot,
  21. vModelCheckbox,
  22. vShow,
  23. withDirectives,
  24. } from '@vue/runtime-dom'
  25. import { type SSRContext, renderToString } from '@vue/server-renderer'
  26. import { PatchFlags } from '@vue/shared'
  27. import { vShowOldKey } from '../../runtime-dom/src/directives/vShow'
  28. function mountWithHydration(html: string, render: () => any) {
  29. const container = document.createElement('div')
  30. container.innerHTML = html
  31. const app = createSSRApp({
  32. render,
  33. })
  34. return {
  35. vnode: app.mount(container).$.subTree as VNode<Node, Element> & {
  36. el: Element
  37. },
  38. container,
  39. }
  40. }
  41. const triggerEvent = (type: string, el: Element) => {
  42. const event = new Event(type)
  43. el.dispatchEvent(event)
  44. }
  45. describe('SSR hydration', () => {
  46. beforeEach(() => {
  47. document.body.innerHTML = ''
  48. })
  49. test('text', async () => {
  50. const msg = ref('foo')
  51. const { vnode, container } = mountWithHydration('foo', () => msg.value)
  52. expect(vnode.el).toBe(container.firstChild)
  53. expect(container.textContent).toBe('foo')
  54. msg.value = 'bar'
  55. await nextTick()
  56. expect(container.textContent).toBe('bar')
  57. })
  58. test('empty text', async () => {
  59. const { container } = mountWithHydration('<div></div>', () =>
  60. h('div', createTextVNode('')),
  61. )
  62. expect(container.textContent).toBe('')
  63. expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
  64. })
  65. test('comment', () => {
  66. const { vnode, container } = mountWithHydration('<!---->', () => null)
  67. expect(vnode.el).toBe(container.firstChild)
  68. expect(vnode.el.nodeType).toBe(8) // comment
  69. })
  70. test('static', () => {
  71. const html = '<div><span>hello</span></div>'
  72. const { vnode, container } = mountWithHydration(html, () =>
  73. createStaticVNode('', 1),
  74. )
  75. expect(vnode.el).toBe(container.firstChild)
  76. expect(vnode.el.outerHTML).toBe(html)
  77. expect(vnode.anchor).toBe(container.firstChild)
  78. expect(vnode.children).toBe(html)
  79. })
  80. test('static (multiple elements)', () => {
  81. const staticContent = '<div></div><span>hello</span>'
  82. const html = `<div><div>hi</div>` + staticContent + `<div>ho</div></div>`
  83. const n1 = h('div', 'hi')
  84. const s = createStaticVNode('', 2)
  85. const n2 = h('div', 'ho')
  86. const { container } = mountWithHydration(html, () => h('div', [n1, s, n2]))
  87. const div = container.firstChild!
  88. expect(n1.el).toBe(div.firstChild)
  89. expect(n2.el).toBe(div.lastChild)
  90. expect(s.el).toBe(div.childNodes[1])
  91. expect(s.anchor).toBe(div.childNodes[2])
  92. expect(s.children).toBe(staticContent)
  93. })
  94. // #6008
  95. test('static (with text node as starting node)', () => {
  96. const html = ` A <span>foo</span> B`
  97. const { vnode, container } = mountWithHydration(html, () =>
  98. createStaticVNode(` A <span>foo</span> B`, 3),
  99. )
  100. expect(vnode.el).toBe(container.firstChild)
  101. expect(vnode.anchor).toBe(container.lastChild)
  102. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  103. })
  104. test('static with content adoption', () => {
  105. const html = ` A <span>foo</span> B`
  106. const { vnode, container } = mountWithHydration(html, () =>
  107. createStaticVNode(``, 3),
  108. )
  109. expect(vnode.el).toBe(container.firstChild)
  110. expect(vnode.anchor).toBe(container.lastChild)
  111. expect(vnode.children).toBe(html)
  112. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  113. })
  114. test('element with text children', async () => {
  115. const msg = ref('foo')
  116. const { vnode, container } = mountWithHydration(
  117. '<div class="foo">foo</div>',
  118. () => h('div', { class: msg.value }, msg.value),
  119. )
  120. expect(vnode.el).toBe(container.firstChild)
  121. expect(container.firstChild!.textContent).toBe('foo')
  122. msg.value = 'bar'
  123. await nextTick()
  124. expect(container.innerHTML).toBe(`<div class="bar">bar</div>`)
  125. })
  126. test('element with elements children', async () => {
  127. const msg = ref('foo')
  128. const fn = vi.fn()
  129. const { vnode, container } = mountWithHydration(
  130. '<div><span>foo</span><span class="foo"></span></div>',
  131. () =>
  132. h('div', [
  133. h('span', msg.value),
  134. h('span', { class: msg.value, onClick: fn }),
  135. ]),
  136. )
  137. expect(vnode.el).toBe(container.firstChild)
  138. expect((vnode.children as VNode[])[0].el).toBe(
  139. container.firstChild!.childNodes[0],
  140. )
  141. expect((vnode.children as VNode[])[1].el).toBe(
  142. container.firstChild!.childNodes[1],
  143. )
  144. // event handler
  145. triggerEvent('click', vnode.el.querySelector('.foo')!)
  146. expect(fn).toHaveBeenCalled()
  147. msg.value = 'bar'
  148. await nextTick()
  149. expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
  150. })
  151. test('element with ref', () => {
  152. const el = ref()
  153. const { vnode, container } = mountWithHydration('<div></div>', () =>
  154. h('div', { ref: el }),
  155. )
  156. expect(vnode.el).toBe(container.firstChild)
  157. expect(el.value).toBe(vnode.el)
  158. })
  159. test('Fragment', async () => {
  160. const msg = ref('foo')
  161. const fn = vi.fn()
  162. const { vnode, container } = mountWithHydration(
  163. '<div><!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]--></div>',
  164. () =>
  165. h('div', [
  166. [
  167. h('span', msg.value),
  168. [h('span', { class: msg.value, onClick: fn })],
  169. ],
  170. ]),
  171. )
  172. expect(vnode.el).toBe(container.firstChild)
  173. expect(vnode.el.innerHTML).toBe(
  174. `<!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]-->`,
  175. )
  176. // start fragment 1
  177. const fragment1 = (vnode.children as VNode[])[0]
  178. expect(fragment1.el).toBe(vnode.el.childNodes[0])
  179. const fragment1Children = fragment1.children as VNode[]
  180. // first <span>
  181. expect(fragment1Children[0].el!.tagName).toBe('SPAN')
  182. expect(fragment1Children[0].el).toBe(vnode.el.childNodes[1])
  183. // start fragment 2
  184. const fragment2 = fragment1Children[1]
  185. expect(fragment2.el).toBe(vnode.el.childNodes[2])
  186. const fragment2Children = fragment2.children as VNode[]
  187. // second <span>
  188. expect(fragment2Children[0].el!.tagName).toBe('SPAN')
  189. expect(fragment2Children[0].el).toBe(vnode.el.childNodes[3])
  190. // end fragment 2
  191. expect(fragment2.anchor).toBe(vnode.el.childNodes[4])
  192. // end fragment 1
  193. expect(fragment1.anchor).toBe(vnode.el.childNodes[5])
  194. // event handler
  195. triggerEvent('click', vnode.el.querySelector('.foo')!)
  196. expect(fn).toHaveBeenCalled()
  197. msg.value = 'bar'
  198. await nextTick()
  199. expect(vnode.el.innerHTML).toBe(
  200. `<!--[--><span>bar</span><!--[--><span class="bar"></span><!--]--><!--]-->`,
  201. )
  202. })
  203. test('Teleport', async () => {
  204. const msg = ref('foo')
  205. const fn = vi.fn()
  206. const teleportContainer = document.createElement('div')
  207. teleportContainer.id = 'teleport'
  208. teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!--teleport anchor-->`
  209. document.body.appendChild(teleportContainer)
  210. const { vnode, container } = mountWithHydration(
  211. '<!--teleport start--><!--teleport end-->',
  212. () =>
  213. h(Teleport, { to: '#teleport' }, [
  214. h('span', msg.value),
  215. h('span', { class: msg.value, onClick: fn }),
  216. ]),
  217. )
  218. expect(vnode.el).toBe(container.firstChild)
  219. expect(vnode.anchor).toBe(container.lastChild)
  220. expect(vnode.target).toBe(teleportContainer)
  221. expect((vnode.children as VNode[])[0].el).toBe(
  222. teleportContainer.childNodes[0],
  223. )
  224. expect((vnode.children as VNode[])[1].el).toBe(
  225. teleportContainer.childNodes[1],
  226. )
  227. expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2])
  228. // event handler
  229. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  230. expect(fn).toHaveBeenCalled()
  231. msg.value = 'bar'
  232. await nextTick()
  233. expect(teleportContainer.innerHTML).toBe(
  234. `<span>bar</span><span class="bar"></span><!--teleport anchor-->`,
  235. )
  236. })
  237. test('Teleport (multiple + integration)', async () => {
  238. const msg = ref('foo')
  239. const fn1 = vi.fn()
  240. const fn2 = vi.fn()
  241. const Comp = () => [
  242. h(Teleport, { to: '#teleport2' }, [
  243. h('span', msg.value),
  244. h('span', { class: msg.value, onClick: fn1 }),
  245. ]),
  246. h(Teleport, { to: '#teleport2' }, [
  247. h('span', msg.value + '2'),
  248. h('span', { class: msg.value + '2', onClick: fn2 }),
  249. ]),
  250. ]
  251. const teleportContainer = document.createElement('div')
  252. teleportContainer.id = 'teleport2'
  253. const ctx: SSRContext = {}
  254. const mainHtml = await renderToString(h(Comp), ctx)
  255. expect(mainHtml).toMatchInlineSnapshot(
  256. `"<!--[--><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--><!--]-->"`,
  257. )
  258. const teleportHtml = ctx.teleports!['#teleport2']
  259. expect(teleportHtml).toMatchInlineSnapshot(
  260. `"<span>foo</span><span class="foo"></span><!--teleport anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
  261. )
  262. teleportContainer.innerHTML = teleportHtml
  263. document.body.appendChild(teleportContainer)
  264. const { vnode, container } = mountWithHydration(mainHtml, Comp)
  265. expect(vnode.el).toBe(container.firstChild)
  266. const teleportVnode1 = (vnode.children as VNode[])[0]
  267. const teleportVnode2 = (vnode.children as VNode[])[1]
  268. expect(teleportVnode1.el).toBe(container.childNodes[1])
  269. expect(teleportVnode1.anchor).toBe(container.childNodes[2])
  270. expect(teleportVnode2.el).toBe(container.childNodes[3])
  271. expect(teleportVnode2.anchor).toBe(container.childNodes[4])
  272. expect(teleportVnode1.target).toBe(teleportContainer)
  273. expect((teleportVnode1 as any).children[0].el).toBe(
  274. teleportContainer.childNodes[0],
  275. )
  276. expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[2])
  277. expect(teleportVnode2.target).toBe(teleportContainer)
  278. expect((teleportVnode2 as any).children[0].el).toBe(
  279. teleportContainer.childNodes[3],
  280. )
  281. expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[5])
  282. // // event handler
  283. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  284. expect(fn1).toHaveBeenCalled()
  285. triggerEvent('click', teleportContainer.querySelector('.foo2')!)
  286. expect(fn2).toHaveBeenCalled()
  287. msg.value = 'bar'
  288. await nextTick()
  289. expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
  290. `"<span>bar</span><span class="bar"></span><!--teleport anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
  291. )
  292. })
  293. test('Teleport (disabled)', async () => {
  294. const msg = ref('foo')
  295. const fn1 = vi.fn()
  296. const fn2 = vi.fn()
  297. const Comp = () => [
  298. h('div', 'foo'),
  299. h(Teleport, { to: '#teleport3', disabled: true }, [
  300. h('span', msg.value),
  301. h('span', { class: msg.value, onClick: fn1 }),
  302. ]),
  303. h('div', { class: msg.value + '2', onClick: fn2 }, 'bar'),
  304. ]
  305. const teleportContainer = document.createElement('div')
  306. teleportContainer.id = 'teleport3'
  307. const ctx: SSRContext = {}
  308. const mainHtml = await renderToString(h(Comp), ctx)
  309. expect(mainHtml).toMatchInlineSnapshot(
  310. `"<!--[--><div>foo</div><!--teleport start--><span>foo</span><span class="foo"></span><!--teleport end--><div class="foo2">bar</div><!--]-->"`,
  311. )
  312. const teleportHtml = ctx.teleports!['#teleport3']
  313. expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)
  314. teleportContainer.innerHTML = teleportHtml
  315. document.body.appendChild(teleportContainer)
  316. const { vnode, container } = mountWithHydration(mainHtml, Comp)
  317. expect(vnode.el).toBe(container.firstChild)
  318. const children = vnode.children as VNode[]
  319. expect(children[0].el).toBe(container.childNodes[1])
  320. const teleportVnode = children[1]
  321. expect(teleportVnode.el).toBe(container.childNodes[2])
  322. expect((teleportVnode.children as VNode[])[0].el).toBe(
  323. container.childNodes[3],
  324. )
  325. expect((teleportVnode.children as VNode[])[1].el).toBe(
  326. container.childNodes[4],
  327. )
  328. expect(teleportVnode.anchor).toBe(container.childNodes[5])
  329. expect(children[2].el).toBe(container.childNodes[6])
  330. expect(teleportVnode.target).toBe(teleportContainer)
  331. expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[0])
  332. // // event handler
  333. triggerEvent('click', container.querySelector('.foo')!)
  334. expect(fn1).toHaveBeenCalled()
  335. triggerEvent('click', container.querySelector('.foo2')!)
  336. expect(fn2).toHaveBeenCalled()
  337. msg.value = 'bar'
  338. await nextTick()
  339. expect(container.innerHTML).toMatchInlineSnapshot(
  340. `"<!--[--><div>foo</div><!--teleport start--><span>bar</span><span class="bar"></span><!--teleport end--><div class="bar2">bar</div><!--]-->"`,
  341. )
  342. })
  343. // #6152
  344. test('Teleport (disabled + as component root)', () => {
  345. const { container } = mountWithHydration(
  346. '<!--[--><div>Parent fragment</div><!--teleport start--><div>Teleport content</div><!--teleport end--><!--]-->',
  347. () => [
  348. h('div', 'Parent fragment'),
  349. h(() =>
  350. h(Teleport, { to: 'body', disabled: true }, [
  351. h('div', 'Teleport content'),
  352. ]),
  353. ),
  354. ],
  355. )
  356. expect(document.body.innerHTML).toBe('')
  357. expect(container.innerHTML).toBe(
  358. '<!--[--><div>Parent fragment</div><!--teleport start--><div>Teleport content</div><!--teleport end--><!--]-->',
  359. )
  360. expect(
  361. `Hydration completed but contains mismatches.`,
  362. ).not.toHaveBeenWarned()
  363. })
  364. test('Teleport (as component root)', () => {
  365. const teleportContainer = document.createElement('div')
  366. teleportContainer.id = 'teleport4'
  367. teleportContainer.innerHTML = `hello<!--teleport anchor-->`
  368. document.body.appendChild(teleportContainer)
  369. const wrapper = {
  370. render() {
  371. return h(Teleport, { to: '#teleport4' }, ['hello'])
  372. },
  373. }
  374. const { vnode, container } = mountWithHydration(
  375. '<div><!--teleport start--><!--teleport end--><div></div></div>',
  376. () => h('div', [h(wrapper), h('div')]),
  377. )
  378. expect(vnode.el).toBe(container.firstChild)
  379. // component el
  380. const wrapperVNode = (vnode as any).children[0]
  381. const tpStart = container.firstChild?.firstChild
  382. const tpEnd = tpStart?.nextSibling
  383. expect(wrapperVNode.el).toBe(tpStart)
  384. expect(wrapperVNode.component.subTree.el).toBe(tpStart)
  385. expect(wrapperVNode.component.subTree.anchor).toBe(tpEnd)
  386. // next node hydrate properly
  387. const nextVNode = (vnode as any).children[1]
  388. expect(nextVNode.el).toBe(container.firstChild?.lastChild)
  389. })
  390. test('Teleport (nested)', () => {
  391. const teleportContainer = document.createElement('div')
  392. teleportContainer.id = 'teleport5'
  393. teleportContainer.innerHTML = `<div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><div>child</div><!--teleport anchor-->`
  394. document.body.appendChild(teleportContainer)
  395. const { vnode, container } = mountWithHydration(
  396. '<!--teleport start--><!--teleport end-->',
  397. () =>
  398. h(Teleport, { to: '#teleport5' }, [
  399. h('div', [h(Teleport, { to: '#teleport5' }, [h('div', 'child')])]),
  400. ]),
  401. )
  402. expect(vnode.el).toBe(container.firstChild)
  403. expect(vnode.anchor).toBe(container.lastChild)
  404. const childDivVNode = (vnode as any).children[0]
  405. const div = teleportContainer.firstChild
  406. expect(childDivVNode.el).toBe(div)
  407. expect(vnode.targetAnchor).toBe(div?.nextSibling)
  408. const childTeleportVNode = childDivVNode.children[0]
  409. expect(childTeleportVNode.el).toBe(div?.firstChild)
  410. expect(childTeleportVNode.anchor).toBe(div?.lastChild)
  411. expect(childTeleportVNode.targetAnchor).toBe(teleportContainer.lastChild)
  412. expect(childTeleportVNode.children[0].el).toBe(
  413. teleportContainer.lastChild?.previousSibling,
  414. )
  415. })
  416. // compile SSR + client render fn from the same template & hydrate
  417. test('full compiler integration', async () => {
  418. const mounted: string[] = []
  419. const log = vi.fn()
  420. const toggle = ref(true)
  421. const Child = {
  422. data() {
  423. return {
  424. count: 0,
  425. text: 'hello',
  426. style: {
  427. color: 'red',
  428. },
  429. }
  430. },
  431. mounted() {
  432. mounted.push('child')
  433. },
  434. template: `
  435. <div>
  436. <span class="count" :style="style">{{ count }}</span>
  437. <button class="inc" @click="count++">inc</button>
  438. <button class="change" @click="style.color = 'green'" >change color</button>
  439. <button class="emit" @click="$emit('foo')">emit</button>
  440. <span class="text">{{ text }}</span>
  441. <input v-model="text">
  442. </div>
  443. `,
  444. }
  445. const App = {
  446. setup() {
  447. return { toggle }
  448. },
  449. mounted() {
  450. mounted.push('parent')
  451. },
  452. template: `
  453. <div>
  454. <span>hello</span>
  455. <template v-if="toggle">
  456. <Child @foo="log('child')"/>
  457. <template v-if="true">
  458. <button class="parent-click" @click="log('click')">click me</button>
  459. </template>
  460. </template>
  461. <span>hello</span>
  462. </div>`,
  463. components: {
  464. Child,
  465. },
  466. methods: {
  467. log,
  468. },
  469. }
  470. const container = document.createElement('div')
  471. // server render
  472. container.innerHTML = await renderToString(h(App))
  473. // hydrate
  474. createSSRApp(App).mount(container)
  475. // assert interactions
  476. // 1. parent button click
  477. triggerEvent('click', container.querySelector('.parent-click')!)
  478. expect(log).toHaveBeenCalledWith('click')
  479. // 2. child inc click + text interpolation
  480. const count = container.querySelector('.count') as HTMLElement
  481. expect(count.textContent).toBe(`0`)
  482. triggerEvent('click', container.querySelector('.inc')!)
  483. await nextTick()
  484. expect(count.textContent).toBe(`1`)
  485. // 3. child color click + style binding
  486. expect(count.style.color).toBe('red')
  487. triggerEvent('click', container.querySelector('.change')!)
  488. await nextTick()
  489. expect(count.style.color).toBe('green')
  490. // 4. child event emit
  491. triggerEvent('click', container.querySelector('.emit')!)
  492. expect(log).toHaveBeenCalledWith('child')
  493. // 5. child v-model
  494. const text = container.querySelector('.text')!
  495. const input = container.querySelector('input')!
  496. expect(text.textContent).toBe('hello')
  497. input.value = 'bye'
  498. triggerEvent('input', input)
  499. await nextTick()
  500. expect(text.textContent).toBe('bye')
  501. })
  502. test('handle click error in ssr mode', async () => {
  503. const App = {
  504. setup() {
  505. const throwError = () => {
  506. throw new Error('Sentry Error')
  507. }
  508. return { throwError }
  509. },
  510. template: `
  511. <div>
  512. <button class="parent-click" @click="throwError">click me</button>
  513. </div>`,
  514. }
  515. const container = document.createElement('div')
  516. // server render
  517. container.innerHTML = await renderToString(h(App))
  518. // hydrate
  519. const app = createSSRApp(App)
  520. const handler = (app.config.errorHandler = vi.fn())
  521. app.mount(container)
  522. // assert interactions
  523. // parent button click
  524. triggerEvent('click', container.querySelector('.parent-click')!)
  525. expect(handler).toHaveBeenCalled()
  526. })
  527. test('handle blur error in ssr mode', async () => {
  528. const App = {
  529. setup() {
  530. const throwError = () => {
  531. throw new Error('Sentry Error')
  532. }
  533. return { throwError }
  534. },
  535. template: `
  536. <div>
  537. <input class="parent-click" @blur="throwError"/>
  538. </div>`,
  539. }
  540. const container = document.createElement('div')
  541. // server render
  542. container.innerHTML = await renderToString(h(App))
  543. // hydrate
  544. const app = createSSRApp(App)
  545. const handler = (app.config.errorHandler = vi.fn())
  546. app.mount(container)
  547. // assert interactions
  548. // parent blur event
  549. triggerEvent('blur', container.querySelector('.parent-click')!)
  550. expect(handler).toHaveBeenCalled()
  551. })
  552. test('Suspense', async () => {
  553. const AsyncChild = {
  554. async setup() {
  555. const count = ref(0)
  556. return () =>
  557. h(
  558. 'span',
  559. {
  560. onClick: () => {
  561. count.value++
  562. },
  563. },
  564. count.value,
  565. )
  566. },
  567. }
  568. const { vnode, container } = mountWithHydration('<span>0</span>', () =>
  569. h(Suspense, () => h(AsyncChild)),
  570. )
  571. expect(vnode.el).toBe(container.firstChild)
  572. // wait for hydration to finish
  573. await new Promise(r => setTimeout(r))
  574. triggerEvent('click', container.querySelector('span')!)
  575. await nextTick()
  576. expect(container.innerHTML).toBe(`<span>1</span>`)
  577. })
  578. test('Suspense (full integration)', async () => {
  579. const mountedCalls: number[] = []
  580. const asyncDeps: Promise<any>[] = []
  581. const AsyncChild = defineComponent({
  582. props: ['n'],
  583. async setup(props) {
  584. const count = ref(props.n)
  585. onMounted(() => {
  586. mountedCalls.push(props.n)
  587. })
  588. const p = new Promise(r => setTimeout(r, props.n * 10))
  589. asyncDeps.push(p)
  590. await p
  591. return () =>
  592. h(
  593. 'span',
  594. {
  595. onClick: () => {
  596. count.value++
  597. },
  598. },
  599. count.value,
  600. )
  601. },
  602. })
  603. const done = vi.fn()
  604. const App = {
  605. template: `
  606. <Suspense @resolve="done">
  607. <div>
  608. <AsyncChild :n="1" />
  609. <AsyncChild :n="2" />
  610. </div>
  611. </Suspense>`,
  612. components: {
  613. AsyncChild,
  614. },
  615. methods: {
  616. done,
  617. },
  618. }
  619. const container = document.createElement('div')
  620. // server render
  621. container.innerHTML = await renderToString(h(App))
  622. expect(container.innerHTML).toMatchInlineSnapshot(
  623. `"<div><span>1</span><span>2</span></div>"`,
  624. )
  625. // reset asyncDeps from ssr
  626. asyncDeps.length = 0
  627. // hydrate
  628. createSSRApp(App).mount(container)
  629. expect(mountedCalls.length).toBe(0)
  630. expect(asyncDeps.length).toBe(2)
  631. // wait for hydration to complete
  632. await Promise.all(asyncDeps)
  633. await new Promise(r => setTimeout(r))
  634. // should flush buffered effects
  635. expect(mountedCalls).toMatchObject([1, 2])
  636. expect(container.innerHTML).toMatch(
  637. `<div><span>1</span><span>2</span></div>`,
  638. )
  639. const span1 = container.querySelector('span')!
  640. triggerEvent('click', span1)
  641. await nextTick()
  642. expect(container.innerHTML).toMatch(
  643. `<div><span>2</span><span>2</span></div>`,
  644. )
  645. const span2 = span1.nextSibling as Element
  646. triggerEvent('click', span2)
  647. await nextTick()
  648. expect(container.innerHTML).toMatch(
  649. `<div><span>2</span><span>3</span></div>`,
  650. )
  651. })
  652. test('async component', async () => {
  653. const spy = vi.fn()
  654. const Comp = () =>
  655. h(
  656. 'button',
  657. {
  658. onClick: spy,
  659. },
  660. 'hello!',
  661. )
  662. let serverResolve: any
  663. let AsyncComp = defineAsyncComponent(
  664. () =>
  665. new Promise(r => {
  666. serverResolve = r
  667. }),
  668. )
  669. const App = {
  670. render() {
  671. return ['hello', h(AsyncComp), 'world']
  672. },
  673. }
  674. // server render
  675. const htmlPromise = renderToString(h(App))
  676. serverResolve(Comp)
  677. const html = await htmlPromise
  678. expect(html).toMatchInlineSnapshot(
  679. `"<!--[-->hello<button>hello!</button>world<!--]-->"`,
  680. )
  681. // hydration
  682. let clientResolve: any
  683. AsyncComp = defineAsyncComponent(
  684. () =>
  685. new Promise(r => {
  686. clientResolve = r
  687. }),
  688. )
  689. const container = document.createElement('div')
  690. container.innerHTML = html
  691. createSSRApp(App).mount(container)
  692. // hydration not complete yet
  693. triggerEvent('click', container.querySelector('button')!)
  694. expect(spy).not.toHaveBeenCalled()
  695. // resolve
  696. clientResolve(Comp)
  697. await new Promise(r => setTimeout(r))
  698. // should be hydrated now
  699. triggerEvent('click', container.querySelector('button')!)
  700. expect(spy).toHaveBeenCalled()
  701. })
  702. test('update async wrapper before resolve', async () => {
  703. const Comp = {
  704. render() {
  705. return h('h1', 'Async component')
  706. },
  707. }
  708. let serverResolve: any
  709. let AsyncComp = defineAsyncComponent(
  710. () =>
  711. new Promise(r => {
  712. serverResolve = r
  713. }),
  714. )
  715. const toggle = ref(true)
  716. const App = {
  717. setup() {
  718. onMounted(() => {
  719. // change state, this makes updateComponent(AsyncComp) execute before
  720. // the async component is resolved
  721. toggle.value = false
  722. })
  723. return () => {
  724. return [toggle.value ? 'hello' : 'world', h(AsyncComp)]
  725. }
  726. },
  727. }
  728. // server render
  729. const htmlPromise = renderToString(h(App))
  730. serverResolve(Comp)
  731. const html = await htmlPromise
  732. expect(html).toMatchInlineSnapshot(
  733. `"<!--[-->hello<h1>Async component</h1><!--]-->"`,
  734. )
  735. // hydration
  736. let clientResolve: any
  737. AsyncComp = defineAsyncComponent(
  738. () =>
  739. new Promise(r => {
  740. clientResolve = r
  741. }),
  742. )
  743. const container = document.createElement('div')
  744. container.innerHTML = html
  745. createSSRApp(App).mount(container)
  746. // resolve
  747. clientResolve(Comp)
  748. await new Promise(r => setTimeout(r))
  749. // should be hydrated now
  750. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  751. expect(container.innerHTML).toMatchInlineSnapshot(
  752. `"<!--[-->world<h1>Async component</h1><!--]-->"`,
  753. )
  754. })
  755. test('hydrate safely when property used by async setup changed before render', async () => {
  756. const toggle = ref(true)
  757. const AsyncComp = {
  758. async setup() {
  759. await new Promise<void>(r => setTimeout(r, 10))
  760. return () => h('h1', 'Async component')
  761. },
  762. }
  763. const AsyncWrapper = {
  764. render() {
  765. return h(AsyncComp)
  766. },
  767. }
  768. const SiblingComp = {
  769. setup() {
  770. toggle.value = false
  771. return () => h('span')
  772. },
  773. }
  774. const App = {
  775. setup() {
  776. return () =>
  777. h(
  778. Suspense,
  779. {},
  780. {
  781. default: () => [
  782. h('main', {}, [
  783. h(AsyncWrapper, {
  784. prop: toggle.value ? 'hello' : 'world',
  785. }),
  786. h(SiblingComp),
  787. ]),
  788. ],
  789. },
  790. )
  791. },
  792. }
  793. // server render
  794. const html = await renderToString(h(App))
  795. expect(html).toMatchInlineSnapshot(
  796. `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
  797. )
  798. expect(toggle.value).toBe(false)
  799. // hydration
  800. // reset the value
  801. toggle.value = true
  802. expect(toggle.value).toBe(true)
  803. const container = document.createElement('div')
  804. container.innerHTML = html
  805. createSSRApp(App).mount(container)
  806. await new Promise(r => setTimeout(r, 10))
  807. expect(toggle.value).toBe(false)
  808. // should be hydrated now
  809. expect(container.innerHTML).toMatchInlineSnapshot(
  810. `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
  811. )
  812. })
  813. test('hydrate safely when property used by deep nested async setup changed before render', async () => {
  814. const toggle = ref(true)
  815. const AsyncComp = {
  816. async setup() {
  817. await new Promise<void>(r => setTimeout(r, 10))
  818. return () => h('h1', 'Async component')
  819. },
  820. }
  821. const AsyncWrapper = { render: () => h(AsyncComp) }
  822. const AsyncWrapperWrapper = { render: () => h(AsyncWrapper) }
  823. const SiblingComp = {
  824. setup() {
  825. toggle.value = false
  826. return () => h('span')
  827. },
  828. }
  829. const App = {
  830. setup() {
  831. return () =>
  832. h(
  833. Suspense,
  834. {},
  835. {
  836. default: () => [
  837. h('main', {}, [
  838. h(AsyncWrapperWrapper, {
  839. prop: toggle.value ? 'hello' : 'world',
  840. }),
  841. h(SiblingComp),
  842. ]),
  843. ],
  844. },
  845. )
  846. },
  847. }
  848. // server render
  849. const html = await renderToString(h(App))
  850. expect(html).toMatchInlineSnapshot(
  851. `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
  852. )
  853. expect(toggle.value).toBe(false)
  854. // hydration
  855. // reset the value
  856. toggle.value = true
  857. expect(toggle.value).toBe(true)
  858. const container = document.createElement('div')
  859. container.innerHTML = html
  860. createSSRApp(App).mount(container)
  861. await new Promise(r => setTimeout(r, 10))
  862. expect(toggle.value).toBe(false)
  863. // should be hydrated now
  864. expect(container.innerHTML).toMatchInlineSnapshot(
  865. `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
  866. )
  867. })
  868. // #3787
  869. test('unmount async wrapper before load', async () => {
  870. let resolve: any
  871. const AsyncComp = defineAsyncComponent(
  872. () =>
  873. new Promise(r => {
  874. resolve = r
  875. }),
  876. )
  877. const show = ref(true)
  878. const root = document.createElement('div')
  879. root.innerHTML = '<div><div>async</div></div>'
  880. createSSRApp({
  881. render() {
  882. return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
  883. },
  884. }).mount(root)
  885. show.value = false
  886. await nextTick()
  887. expect(root.innerHTML).toBe('<div><div>hi</div></div>')
  888. resolve({})
  889. })
  890. test('unmount async wrapper before load (fragment)', async () => {
  891. let resolve: any
  892. const AsyncComp = defineAsyncComponent(
  893. () =>
  894. new Promise(r => {
  895. resolve = r
  896. }),
  897. )
  898. const show = ref(true)
  899. const root = document.createElement('div')
  900. root.innerHTML = '<div><!--[-->async<!--]--></div>'
  901. createSSRApp({
  902. render() {
  903. return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
  904. },
  905. }).mount(root)
  906. show.value = false
  907. await nextTick()
  908. expect(root.innerHTML).toBe('<div><div>hi</div></div>')
  909. resolve({})
  910. })
  911. test('elements with camel-case in svg ', () => {
  912. const { vnode, container } = mountWithHydration(
  913. '<animateTransform></animateTransform>',
  914. () => h('animateTransform'),
  915. )
  916. expect(vnode.el).toBe(container.firstChild)
  917. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  918. })
  919. test('SVG as a mount container', () => {
  920. const svgContainer = document.createElement('svg')
  921. svgContainer.innerHTML = '<g></g>'
  922. const app = createSSRApp({
  923. render: () => h('g'),
  924. })
  925. expect(
  926. (
  927. app.mount(svgContainer).$.subTree as VNode<Node, Element> & {
  928. el: Element
  929. }
  930. ).el instanceof SVGElement,
  931. )
  932. })
  933. test('force hydrate prop with `.prop` modifier', () => {
  934. const { container } = mountWithHydration(
  935. '<input type="checkbox" :indeterminate.prop="true">',
  936. () =>
  937. h('input', {
  938. type: 'checkbox',
  939. '.indeterminate': true,
  940. }),
  941. )
  942. expect((container.firstChild! as any).indeterminate).toBe(true)
  943. })
  944. test('force hydrate input v-model with non-string value bindings', () => {
  945. const { container } = mountWithHydration(
  946. '<input type="checkbox" value="true">',
  947. () =>
  948. withDirectives(
  949. createVNode(
  950. 'input',
  951. { type: 'checkbox', 'true-value': true },
  952. null,
  953. PatchFlags.PROPS,
  954. ['true-value'],
  955. ),
  956. [[vModelCheckbox, true]],
  957. ),
  958. )
  959. expect((container.firstChild as any)._trueValue).toBe(true)
  960. })
  961. test('force hydrate checkbox with indeterminate', () => {
  962. const { container } = mountWithHydration(
  963. '<input type="checkbox" indeterminate>',
  964. () =>
  965. createVNode(
  966. 'input',
  967. { type: 'checkbox', indeterminate: '' },
  968. null,
  969. PatchFlags.HOISTED,
  970. ),
  971. )
  972. expect((container.firstChild as any).indeterminate).toBe(true)
  973. })
  974. test('force hydrate select option with non-string value bindings', () => {
  975. const { container } = mountWithHydration(
  976. '<select><option value="true">ok</option></select>',
  977. () =>
  978. h('select', [
  979. // hoisted because bound value is a constant...
  980. createVNode('option', { value: true }, null, -1 /* HOISTED */),
  981. ]),
  982. )
  983. expect((container.firstChild!.firstChild as any)._value).toBe(true)
  984. })
  985. // #5728
  986. test('empty text node in slot', () => {
  987. const Comp = {
  988. render(this: any) {
  989. return renderSlot(this.$slots, 'default', {}, () => [
  990. createTextVNode(''),
  991. ])
  992. },
  993. }
  994. const { container, vnode } = mountWithHydration('<!--[--><!--]-->', () =>
  995. h(Comp),
  996. )
  997. expect(container.childNodes.length).toBe(3)
  998. const text = container.childNodes[1]
  999. expect(text.nodeType).toBe(3)
  1000. expect(vnode.el).toBe(container.childNodes[0])
  1001. // component => slot fragment => text node
  1002. expect((vnode as any).component?.subTree.children[0].el).toBe(text)
  1003. })
  1004. test('app.unmount()', async () => {
  1005. const container = document.createElement('DIV')
  1006. container.innerHTML = '<button></button>'
  1007. const App = defineComponent({
  1008. setup(_, { expose }) {
  1009. const count = ref(0)
  1010. expose({ count })
  1011. return () =>
  1012. h('button', {
  1013. onClick: () => count.value++,
  1014. })
  1015. },
  1016. })
  1017. const app = createSSRApp(App)
  1018. const vm = app.mount(container)
  1019. await nextTick()
  1020. expect((container as any)._vnode).toBeDefined()
  1021. // @ts-expect-error - expose()'d properties are not available on vm type
  1022. expect(vm.count).toBe(0)
  1023. app.unmount()
  1024. expect((container as any)._vnode).toBe(null)
  1025. })
  1026. // #6637
  1027. test('stringified root fragment', () => {
  1028. mountWithHydration(`<!--[--><div></div><!--]-->`, () =>
  1029. createStaticVNode(`<div></div>`, 1),
  1030. )
  1031. expect(`mismatch`).not.toHaveBeenWarned()
  1032. })
  1033. test('transition appear', () => {
  1034. const { vnode, container } = mountWithHydration(
  1035. `<template><div>foo</div></template>`,
  1036. () =>
  1037. h(
  1038. Transition,
  1039. { appear: true },
  1040. {
  1041. default: () => h('div', 'foo'),
  1042. },
  1043. ),
  1044. )
  1045. expect(container.firstChild).toMatchInlineSnapshot(`
  1046. <div
  1047. class="v-enter-from v-enter-active"
  1048. >
  1049. foo
  1050. </div>
  1051. `)
  1052. expect(vnode.el).toBe(container.firstChild)
  1053. expect(`mismatch`).not.toHaveBeenWarned()
  1054. })
  1055. test('transition appear with v-if', () => {
  1056. const show = false
  1057. const { vnode, container } = mountWithHydration(
  1058. `<template><!----></template>`,
  1059. () =>
  1060. h(
  1061. Transition,
  1062. { appear: true },
  1063. {
  1064. default: () => (show ? h('div', 'foo') : createCommentVNode('')),
  1065. },
  1066. ),
  1067. )
  1068. expect(container.firstChild).toMatchInlineSnapshot('<!---->')
  1069. expect(vnode.el).toBe(container.firstChild)
  1070. expect(`mismatch`).not.toHaveBeenWarned()
  1071. })
  1072. test('transition appear with v-show', () => {
  1073. const show = false
  1074. const { vnode, container } = mountWithHydration(
  1075. `<template><div style="display: none;">foo</div></template>`,
  1076. () =>
  1077. h(
  1078. Transition,
  1079. { appear: true },
  1080. {
  1081. default: () =>
  1082. withDirectives(createVNode('div', null, 'foo'), [[vShow, show]]),
  1083. },
  1084. ),
  1085. )
  1086. expect(container.firstChild).toMatchInlineSnapshot(`
  1087. <div
  1088. class="v-enter-from v-enter-active"
  1089. style="display: none;"
  1090. >
  1091. foo
  1092. </div>
  1093. `)
  1094. expect((container.firstChild as any)[vShowOldKey]).toBe('')
  1095. expect(vnode.el).toBe(container.firstChild)
  1096. expect(`mismatch`).not.toHaveBeenWarned()
  1097. })
  1098. test('transition appear w/ event listener', async () => {
  1099. const container = document.createElement('div')
  1100. container.innerHTML = `<template><button>0</button></template>`
  1101. createSSRApp({
  1102. data() {
  1103. return {
  1104. count: 0,
  1105. }
  1106. },
  1107. template: `
  1108. <Transition appear>
  1109. <button @click="count++">{{count}}</button>
  1110. </Transition>
  1111. `,
  1112. }).mount(container)
  1113. expect(container.firstChild).toMatchInlineSnapshot(`
  1114. <button
  1115. class="v-enter-from v-enter-active"
  1116. >
  1117. 0
  1118. </button>
  1119. `)
  1120. triggerEvent('click', container.querySelector('button')!)
  1121. await nextTick()
  1122. expect(container.firstChild).toMatchInlineSnapshot(`
  1123. <button
  1124. class="v-enter-from v-enter-active"
  1125. >
  1126. 1
  1127. </button>
  1128. `)
  1129. })
  1130. describe('mismatch handling', () => {
  1131. test('text node', () => {
  1132. const { container } = mountWithHydration(`foo`, () => 'bar')
  1133. expect(container.textContent).toBe('bar')
  1134. expect(`Hydration text mismatch`).toHaveBeenWarned()
  1135. })
  1136. test('element text content', () => {
  1137. const { container } = mountWithHydration(`<div>foo</div>`, () =>
  1138. h('div', 'bar'),
  1139. )
  1140. expect(container.innerHTML).toBe('<div>bar</div>')
  1141. expect(`Hydration text content mismatch`).toHaveBeenWarned()
  1142. })
  1143. test('not enough children', () => {
  1144. const { container } = mountWithHydration(`<div></div>`, () =>
  1145. h('div', [h('span', 'foo'), h('span', 'bar')]),
  1146. )
  1147. expect(container.innerHTML).toBe(
  1148. '<div><span>foo</span><span>bar</span></div>',
  1149. )
  1150. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1151. })
  1152. test('too many children', () => {
  1153. const { container } = mountWithHydration(
  1154. `<div><span>foo</span><span>bar</span></div>`,
  1155. () => h('div', [h('span', 'foo')]),
  1156. )
  1157. expect(container.innerHTML).toBe('<div><span>foo</span></div>')
  1158. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1159. })
  1160. test('complete mismatch', () => {
  1161. const { container } = mountWithHydration(
  1162. `<div><span>foo</span><span>bar</span></div>`,
  1163. () => h('div', [h('div', 'foo'), h('p', 'bar')]),
  1164. )
  1165. expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
  1166. expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
  1167. })
  1168. test('fragment mismatch removal', () => {
  1169. const { container } = mountWithHydration(
  1170. `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  1171. () => h('div', [h('span', 'replaced')]),
  1172. )
  1173. expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
  1174. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1175. })
  1176. test('fragment not enough children', () => {
  1177. const { container } = mountWithHydration(
  1178. `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  1179. () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
  1180. )
  1181. expect(container.innerHTML).toBe(
  1182. '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
  1183. )
  1184. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1185. })
  1186. test('fragment too many children', () => {
  1187. const { container } = mountWithHydration(
  1188. `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  1189. () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
  1190. )
  1191. expect(container.innerHTML).toBe(
  1192. '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>',
  1193. )
  1194. // fragment ends early and attempts to hydrate the extra <div>bar</div>
  1195. // as 2nd fragment child.
  1196. expect(`Hydration text content mismatch`).toHaveBeenWarned()
  1197. // excessive children removal
  1198. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1199. })
  1200. test('Teleport target has empty children', () => {
  1201. const teleportContainer = document.createElement('div')
  1202. teleportContainer.id = 'teleport'
  1203. document.body.appendChild(teleportContainer)
  1204. mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
  1205. h(Teleport, { to: '#teleport' }, [h('span', 'value')]),
  1206. )
  1207. expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
  1208. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1209. })
  1210. test('comment mismatch (element)', () => {
  1211. const { container } = mountWithHydration(`<div><span></span></div>`, () =>
  1212. h('div', [createCommentVNode('hi')]),
  1213. )
  1214. expect(container.innerHTML).toBe('<div><!--hi--></div>')
  1215. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1216. })
  1217. test('comment mismatch (text)', () => {
  1218. const { container } = mountWithHydration(`<div>foobar</div>`, () =>
  1219. h('div', [createCommentVNode('hi')]),
  1220. )
  1221. expect(container.innerHTML).toBe('<div><!--hi--></div>')
  1222. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1223. })
  1224. test('class mismatch', () => {
  1225. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1226. h('div', { class: ['foo', 'bar'] }),
  1227. )
  1228. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1229. h('div', { class: { foo: true, bar: true } }),
  1230. )
  1231. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1232. h('div', { class: 'foo bar' }),
  1233. )
  1234. // SVG classes
  1235. mountWithHydration(`<svg class="foo bar"></svg>`, () =>
  1236. h('svg', { class: 'foo bar' }),
  1237. )
  1238. // class with different order
  1239. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1240. h('div', { class: 'bar foo' }),
  1241. )
  1242. expect(`Hydration class mismatch`).not.toHaveBeenWarned()
  1243. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1244. h('div', { class: 'foo' }),
  1245. )
  1246. expect(`Hydration class mismatch`).toHaveBeenWarned()
  1247. })
  1248. test('style mismatch', () => {
  1249. mountWithHydration(`<div style="color:red;"></div>`, () =>
  1250. h('div', { style: { color: 'red' } }),
  1251. )
  1252. mountWithHydration(`<div style="color:red;"></div>`, () =>
  1253. h('div', { style: `color:red;` }),
  1254. )
  1255. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  1256. mountWithHydration(`<div style="color:red;"></div>`, () =>
  1257. h('div', { style: { color: 'green' } }),
  1258. )
  1259. expect(`Hydration style mismatch`).toHaveBeenWarned()
  1260. })
  1261. test('attr mismatch', () => {
  1262. mountWithHydration(`<div id="foo"></div>`, () => h('div', { id: 'foo' }))
  1263. mountWithHydration(`<div spellcheck></div>`, () =>
  1264. h('div', { spellcheck: '' }),
  1265. )
  1266. mountWithHydration(`<div></div>`, () => h('div', { id: undefined }))
  1267. // boolean
  1268. mountWithHydration(`<select multiple></div>`, () =>
  1269. h('select', { multiple: true }),
  1270. )
  1271. mountWithHydration(`<select multiple></div>`, () =>
  1272. h('select', { multiple: 'multiple' }),
  1273. )
  1274. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1275. mountWithHydration(`<div></div>`, () => h('div', { id: 'foo' }))
  1276. expect(`Hydration attribute mismatch`).toHaveBeenWarned()
  1277. mountWithHydration(`<div id="bar"></div>`, () => h('div', { id: 'foo' }))
  1278. expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2)
  1279. })
  1280. })
  1281. })