hydration.spec.ts 76 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630
  1. /**
  2. * @vitest-environment jsdom
  3. */
  4. import {
  5. type ObjectDirective,
  6. Suspense,
  7. Teleport,
  8. Transition,
  9. type VNode,
  10. createBlock,
  11. createCommentVNode,
  12. createElementBlock,
  13. createElementVNode,
  14. createSSRApp,
  15. createStaticVNode,
  16. createTextVNode,
  17. createVNode,
  18. defineAsyncComponent,
  19. defineComponent,
  20. h,
  21. nextTick,
  22. onMounted,
  23. onServerPrefetch,
  24. openBlock,
  25. reactive,
  26. ref,
  27. renderSlot,
  28. useCssVars,
  29. vModelCheckbox,
  30. vShow,
  31. withCtx,
  32. withDirectives,
  33. } from '@vue/runtime-dom'
  34. import type { HMRRuntime } from '../src/hmr'
  35. import { type SSRContext, renderToString } from '@vue/server-renderer'
  36. import { PatchFlags, normalizeStyle } from '@vue/shared'
  37. import { vShowOriginalDisplay } from '../../runtime-dom/src/directives/vShow'
  38. declare var __VUE_HMR_RUNTIME__: HMRRuntime
  39. const { createRecord, reload } = __VUE_HMR_RUNTIME__
  40. function mountWithHydration(html: string, render: () => any) {
  41. const container = document.createElement('div')
  42. container.innerHTML = html
  43. const app = createSSRApp({
  44. render,
  45. })
  46. return {
  47. vnode: app.mount(container).$.subTree as VNode<Node, Element> & {
  48. el: Element
  49. },
  50. container,
  51. }
  52. }
  53. const triggerEvent = (type: string, el: Element) => {
  54. const event = new Event(type)
  55. el.dispatchEvent(event)
  56. }
  57. describe('SSR hydration', () => {
  58. beforeEach(() => {
  59. document.body.innerHTML = ''
  60. })
  61. test('text', async () => {
  62. const msg = ref('foo')
  63. const { vnode, container } = mountWithHydration('foo', () => msg.value)
  64. expect(vnode.el).toBe(container.firstChild)
  65. expect(container.textContent).toBe('foo')
  66. msg.value = 'bar'
  67. await nextTick()
  68. expect(container.textContent).toBe('bar')
  69. })
  70. test('empty text', async () => {
  71. const { container } = mountWithHydration('<div></div>', () =>
  72. h('div', createTextVNode('')),
  73. )
  74. expect(container.textContent).toBe('')
  75. expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
  76. })
  77. test('text w/ newlines', async () => {
  78. mountWithHydration('<div>1\n2\n3</div>', () => h('div', '1\r\n2\r3'))
  79. expect(`Hydration text mismatch`).not.toHaveBeenWarned()
  80. })
  81. test('comment', () => {
  82. const { vnode, container } = mountWithHydration('<!---->', () => null)
  83. expect(vnode.el).toBe(container.firstChild)
  84. expect(vnode.el.nodeType).toBe(8) // comment
  85. })
  86. test('static', () => {
  87. const html = '<div><span>hello</span></div>'
  88. const { vnode, container } = mountWithHydration(html, () =>
  89. createStaticVNode('', 1),
  90. )
  91. expect(vnode.el).toBe(container.firstChild)
  92. expect(vnode.el.outerHTML).toBe(html)
  93. expect(vnode.anchor).toBe(container.firstChild)
  94. expect(vnode.children).toBe(html)
  95. })
  96. test('static (multiple elements)', () => {
  97. const staticContent = '<div></div><span>hello</span>'
  98. const html = `<div><div>hi</div>` + staticContent + `<div>ho</div></div>`
  99. const n1 = h('div', 'hi')
  100. const s = createStaticVNode('', 2)
  101. const n2 = h('div', 'ho')
  102. const { container } = mountWithHydration(html, () => h('div', [n1, s, n2]))
  103. const div = container.firstChild!
  104. expect(n1.el).toBe(div.firstChild)
  105. expect(n2.el).toBe(div.lastChild)
  106. expect(s.el).toBe(div.childNodes[1])
  107. expect(s.anchor).toBe(div.childNodes[2])
  108. expect(s.children).toBe(staticContent)
  109. })
  110. // #6008
  111. test('static (with text node as starting node)', () => {
  112. const html = ` A <span>foo</span> B`
  113. const { vnode, container } = mountWithHydration(html, () =>
  114. createStaticVNode(` A <span>foo</span> B`, 3),
  115. )
  116. expect(vnode.el).toBe(container.firstChild)
  117. expect(vnode.anchor).toBe(container.lastChild)
  118. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  119. })
  120. test('static with content adoption', () => {
  121. const html = ` A <span>foo</span> B`
  122. const { vnode, container } = mountWithHydration(html, () =>
  123. createStaticVNode(``, 3),
  124. )
  125. expect(vnode.el).toBe(container.firstChild)
  126. expect(vnode.anchor).toBe(container.lastChild)
  127. expect(vnode.children).toBe(html)
  128. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  129. })
  130. test('element with text children', async () => {
  131. const msg = ref('foo')
  132. const { vnode, container } = mountWithHydration(
  133. '<div class="foo">foo</div>',
  134. () => h('div', { class: msg.value }, msg.value),
  135. )
  136. expect(vnode.el).toBe(container.firstChild)
  137. expect(container.firstChild!.textContent).toBe('foo')
  138. msg.value = 'bar'
  139. await nextTick()
  140. expect(container.innerHTML).toBe(`<div class="bar">bar</div>`)
  141. })
  142. // #7285
  143. test('element with multiple continuous text vnodes', async () => {
  144. // should no mismatch warning
  145. const { container } = mountWithHydration('<div>foo0o</div>', () =>
  146. h('div', ['fo', createTextVNode('o'), 0, 'o']),
  147. )
  148. expect(container.textContent).toBe('foo0o')
  149. })
  150. test('element with elements children', async () => {
  151. const msg = ref('foo')
  152. const fn = vi.fn()
  153. const { vnode, container } = mountWithHydration(
  154. '<div><span>foo</span><span class="foo"></span></div>',
  155. () =>
  156. h('div', [
  157. h('span', msg.value),
  158. h('span', { class: msg.value, onClick: fn }),
  159. ]),
  160. )
  161. expect(vnode.el).toBe(container.firstChild)
  162. expect((vnode.children as VNode[])[0].el).toBe(
  163. container.firstChild!.childNodes[0],
  164. )
  165. expect((vnode.children as VNode[])[1].el).toBe(
  166. container.firstChild!.childNodes[1],
  167. )
  168. // event handler
  169. triggerEvent('click', vnode.el.querySelector('.foo')!)
  170. expect(fn).toHaveBeenCalled()
  171. msg.value = 'bar'
  172. await nextTick()
  173. expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
  174. })
  175. test('element with ref', () => {
  176. const el = ref()
  177. const { vnode, container } = mountWithHydration('<div></div>', () =>
  178. h('div', { ref: el }),
  179. )
  180. expect(vnode.el).toBe(container.firstChild)
  181. expect(el.value).toBe(vnode.el)
  182. })
  183. test('Fragment', async () => {
  184. const msg = ref('foo')
  185. const fn = vi.fn()
  186. const { vnode, container } = mountWithHydration(
  187. '<div><!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]--></div>',
  188. () =>
  189. h('div', [
  190. [
  191. h('span', msg.value),
  192. [h('span', { class: msg.value, onClick: fn })],
  193. ],
  194. ]),
  195. )
  196. expect(vnode.el).toBe(container.firstChild)
  197. expect(vnode.el.innerHTML).toBe(
  198. `<!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]-->`,
  199. )
  200. // start fragment 1
  201. const fragment1 = (vnode.children as VNode[])[0]
  202. expect(fragment1.el).toBe(vnode.el.childNodes[0])
  203. const fragment1Children = fragment1.children as VNode[]
  204. // first <span>
  205. expect(fragment1Children[0].el!.tagName).toBe('SPAN')
  206. expect(fragment1Children[0].el).toBe(vnode.el.childNodes[1])
  207. // start fragment 2
  208. const fragment2 = fragment1Children[1]
  209. expect(fragment2.el).toBe(vnode.el.childNodes[2])
  210. const fragment2Children = fragment2.children as VNode[]
  211. // second <span>
  212. expect(fragment2Children[0].el!.tagName).toBe('SPAN')
  213. expect(fragment2Children[0].el).toBe(vnode.el.childNodes[3])
  214. // end fragment 2
  215. expect(fragment2.anchor).toBe(vnode.el.childNodes[4])
  216. // end fragment 1
  217. expect(fragment1.anchor).toBe(vnode.el.childNodes[5])
  218. // event handler
  219. triggerEvent('click', vnode.el.querySelector('.foo')!)
  220. expect(fn).toHaveBeenCalled()
  221. msg.value = 'bar'
  222. await nextTick()
  223. expect(vnode.el.innerHTML).toBe(
  224. `<!--[--><span>bar</span><!--[--><span class="bar"></span><!--]--><!--]-->`,
  225. )
  226. })
  227. // #7285
  228. test('Fragment (multiple continuous text vnodes)', async () => {
  229. // should no mismatch warning
  230. const { container } = mountWithHydration('<!--[-->fooo<!--]-->', () => [
  231. 'fo',
  232. createTextVNode('o'),
  233. 'o',
  234. ])
  235. expect(container.textContent).toBe('fooo')
  236. })
  237. test('Teleport', async () => {
  238. const msg = ref('foo')
  239. const fn = vi.fn()
  240. const teleportContainer = document.createElement('div')
  241. teleportContainer.id = 'teleport'
  242. teleportContainer.innerHTML = `<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor-->`
  243. document.body.appendChild(teleportContainer)
  244. const { vnode, container } = mountWithHydration(
  245. '<!--teleport start--><!--teleport end-->',
  246. () =>
  247. h(Teleport, { to: '#teleport' }, [
  248. h('span', msg.value),
  249. h('span', { class: msg.value, onClick: fn }),
  250. ]),
  251. )
  252. expect(vnode.el).toBe(container.firstChild)
  253. expect(vnode.anchor).toBe(container.lastChild)
  254. expect(vnode.target).toBe(teleportContainer)
  255. expect(vnode.targetStart).toBe(teleportContainer.childNodes[0])
  256. expect((vnode.children as VNode[])[0].el).toBe(
  257. teleportContainer.childNodes[1],
  258. )
  259. expect((vnode.children as VNode[])[1].el).toBe(
  260. teleportContainer.childNodes[2],
  261. )
  262. expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[3])
  263. // event handler
  264. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  265. expect(fn).toHaveBeenCalled()
  266. msg.value = 'bar'
  267. await nextTick()
  268. expect(teleportContainer.innerHTML).toBe(
  269. `<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor-->`,
  270. )
  271. })
  272. test('Teleport (multiple + integration)', async () => {
  273. const msg = ref('foo')
  274. const fn1 = vi.fn()
  275. const fn2 = vi.fn()
  276. const Comp = () => [
  277. h(Teleport, { to: '#teleport2' }, [
  278. h('span', msg.value),
  279. h('span', { class: msg.value, onClick: fn1 }),
  280. ]),
  281. h(Teleport, { to: '#teleport2' }, [
  282. h('span', msg.value + '2'),
  283. h('span', { class: msg.value + '2', onClick: fn2 }),
  284. ]),
  285. ]
  286. const teleportContainer = document.createElement('div')
  287. teleportContainer.id = 'teleport2'
  288. const ctx: SSRContext = {}
  289. const mainHtml = await renderToString(h(Comp), ctx)
  290. expect(mainHtml).toMatchInlineSnapshot(
  291. `"<!--[--><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--><!--]-->"`,
  292. )
  293. const teleportHtml = ctx.teleports!['#teleport2']
  294. expect(teleportHtml).toMatchInlineSnapshot(
  295. `"<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor--><!--teleport start anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
  296. )
  297. teleportContainer.innerHTML = teleportHtml
  298. document.body.appendChild(teleportContainer)
  299. const { vnode, container } = mountWithHydration(mainHtml, Comp)
  300. expect(vnode.el).toBe(container.firstChild)
  301. const teleportVnode1 = (vnode.children as VNode[])[0]
  302. const teleportVnode2 = (vnode.children as VNode[])[1]
  303. expect(teleportVnode1.el).toBe(container.childNodes[1])
  304. expect(teleportVnode1.anchor).toBe(container.childNodes[2])
  305. expect(teleportVnode2.el).toBe(container.childNodes[3])
  306. expect(teleportVnode2.anchor).toBe(container.childNodes[4])
  307. expect(teleportVnode1.target).toBe(teleportContainer)
  308. expect(teleportVnode1.targetStart).toBe(teleportContainer.childNodes[0])
  309. expect((teleportVnode1 as any).children[0].el).toBe(
  310. teleportContainer.childNodes[1],
  311. )
  312. expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[3])
  313. expect(teleportVnode2.target).toBe(teleportContainer)
  314. expect(teleportVnode2.targetStart).toBe(teleportContainer.childNodes[4])
  315. expect((teleportVnode2 as any).children[0].el).toBe(
  316. teleportContainer.childNodes[5],
  317. )
  318. expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[7])
  319. // // event handler
  320. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  321. expect(fn1).toHaveBeenCalled()
  322. triggerEvent('click', teleportContainer.querySelector('.foo2')!)
  323. expect(fn2).toHaveBeenCalled()
  324. msg.value = 'bar'
  325. await nextTick()
  326. expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
  327. `"<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor--><!--teleport start anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
  328. )
  329. })
  330. test('Teleport (disabled)', async () => {
  331. const msg = ref('foo')
  332. const fn1 = vi.fn()
  333. const fn2 = vi.fn()
  334. const Comp = () => [
  335. h('div', 'foo'),
  336. h(Teleport, { to: '#teleport3', disabled: true }, [
  337. h('span', msg.value),
  338. h('span', { class: msg.value, onClick: fn1 }),
  339. ]),
  340. h('div', { class: msg.value + '2', onClick: fn2 }, 'bar'),
  341. ]
  342. const teleportContainer = document.createElement('div')
  343. teleportContainer.id = 'teleport3'
  344. const ctx: SSRContext = {}
  345. const mainHtml = await renderToString(h(Comp), ctx)
  346. expect(mainHtml).toMatchInlineSnapshot(
  347. `"<!--[--><div>foo</div><!--teleport start--><span>foo</span><span class="foo"></span><!--teleport end--><div class="foo2">bar</div><!--]-->"`,
  348. )
  349. const teleportHtml = ctx.teleports!['#teleport3']
  350. expect(teleportHtml).toMatchInlineSnapshot(
  351. `"<!--teleport start anchor--><!--teleport anchor-->"`,
  352. )
  353. teleportContainer.innerHTML = teleportHtml
  354. document.body.appendChild(teleportContainer)
  355. const { vnode, container } = mountWithHydration(mainHtml, Comp)
  356. expect(vnode.el).toBe(container.firstChild)
  357. const children = vnode.children as VNode[]
  358. expect(children[0].el).toBe(container.childNodes[1])
  359. const teleportVnode = children[1]
  360. expect(teleportVnode.el).toBe(container.childNodes[2])
  361. expect((teleportVnode.children as VNode[])[0].el).toBe(
  362. container.childNodes[3],
  363. )
  364. expect((teleportVnode.children as VNode[])[1].el).toBe(
  365. container.childNodes[4],
  366. )
  367. expect(teleportVnode.anchor).toBe(container.childNodes[5])
  368. expect(children[2].el).toBe(container.childNodes[6])
  369. expect(teleportVnode.target).toBe(teleportContainer)
  370. expect(teleportVnode.targetStart).toBe(teleportContainer.childNodes[0])
  371. expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[1])
  372. // // event handler
  373. triggerEvent('click', container.querySelector('.foo')!)
  374. expect(fn1).toHaveBeenCalled()
  375. triggerEvent('click', container.querySelector('.foo2')!)
  376. expect(fn2).toHaveBeenCalled()
  377. msg.value = 'bar'
  378. await nextTick()
  379. expect(container.innerHTML).toMatchInlineSnapshot(
  380. `"<!--[--><div>foo</div><!--teleport start--><span>bar</span><span class="bar"></span><!--teleport end--><div class="bar2">bar</div><!--]-->"`,
  381. )
  382. })
  383. // #6152
  384. test('Teleport (disabled + as component root)', () => {
  385. const { container } = mountWithHydration(
  386. '<!--[--><div>Parent fragment</div><!--teleport start--><div>Teleport content</div><!--teleport end--><!--]-->',
  387. () => [
  388. h('div', 'Parent fragment'),
  389. h(() =>
  390. h(Teleport, { to: 'body', disabled: true }, [
  391. h('div', 'Teleport content'),
  392. ]),
  393. ),
  394. ],
  395. )
  396. expect(document.body.innerHTML).toBe('')
  397. expect(container.innerHTML).toBe(
  398. '<!--[--><div>Parent fragment</div><!--teleport start--><div>Teleport content</div><!--teleport end--><!--]-->',
  399. )
  400. expect(
  401. `Hydration completed but contains mismatches.`,
  402. ).not.toHaveBeenWarned()
  403. })
  404. test('Teleport (as component root)', () => {
  405. const teleportContainer = document.createElement('div')
  406. teleportContainer.id = 'teleport4'
  407. teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
  408. document.body.appendChild(teleportContainer)
  409. const wrapper = {
  410. render() {
  411. return h(Teleport, { to: '#teleport4' }, ['hello'])
  412. },
  413. }
  414. const { vnode, container } = mountWithHydration(
  415. '<div><!--teleport start--><!--teleport end--><div></div></div>',
  416. () => h('div', [h(wrapper), h('div')]),
  417. )
  418. expect(vnode.el).toBe(container.firstChild)
  419. // component el
  420. const wrapperVNode = (vnode as any).children[0]
  421. const tpStart = container.firstChild?.firstChild
  422. const tpEnd = tpStart?.nextSibling
  423. expect(wrapperVNode.el).toBe(tpStart)
  424. expect(wrapperVNode.component.subTree.el).toBe(tpStart)
  425. expect(wrapperVNode.component.subTree.anchor).toBe(tpEnd)
  426. // next node hydrate properly
  427. const nextVNode = (vnode as any).children[1]
  428. expect(nextVNode.el).toBe(container.firstChild?.lastChild)
  429. })
  430. test('Teleport (nested)', () => {
  431. const teleportContainer = document.createElement('div')
  432. teleportContainer.id = 'teleport5'
  433. teleportContainer.innerHTML = `<!--teleport start anchor--><div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><!--teleport start anchor--><div>child</div><!--teleport anchor-->`
  434. document.body.appendChild(teleportContainer)
  435. const { vnode, container } = mountWithHydration(
  436. '<!--teleport start--><!--teleport end-->',
  437. () =>
  438. h(Teleport, { to: '#teleport5' }, [
  439. h('div', [h(Teleport, { to: '#teleport5' }, [h('div', 'child')])]),
  440. ]),
  441. )
  442. expect(vnode.el).toBe(container.firstChild)
  443. expect(vnode.anchor).toBe(container.lastChild)
  444. const childDivVNode = (vnode as any).children[0]
  445. const div = teleportContainer.childNodes[1]
  446. expect(childDivVNode.el).toBe(div)
  447. expect(vnode.targetAnchor).toBe(div?.nextSibling)
  448. const childTeleportVNode = childDivVNode.children[0]
  449. expect(childTeleportVNode.el).toBe(div?.firstChild)
  450. expect(childTeleportVNode.anchor).toBe(div?.lastChild)
  451. expect(childTeleportVNode.targetAnchor).toBe(teleportContainer.lastChild)
  452. expect(childTeleportVNode.children[0].el).toBe(
  453. teleportContainer.lastChild?.previousSibling,
  454. )
  455. })
  456. test('with data-allow-mismatch component when using onServerPrefetch', async () => {
  457. const Comp = {
  458. template: `
  459. <div>Comp2</div>
  460. `,
  461. }
  462. let foo: any
  463. const App = {
  464. setup() {
  465. const flag = ref(true)
  466. foo = () => {
  467. flag.value = false
  468. }
  469. onServerPrefetch(() => (flag.value = false))
  470. return { flag }
  471. },
  472. components: {
  473. Comp,
  474. },
  475. template: `
  476. <span data-allow-mismatch>
  477. <Comp v-if="flag"></Comp>
  478. </span>
  479. `,
  480. }
  481. // hydrate
  482. const container = document.createElement('div')
  483. container.innerHTML = await renderToString(h(App))
  484. createSSRApp(App).mount(container)
  485. expect(container.innerHTML).toBe(
  486. '<span data-allow-mismatch=""><div>Comp2</div></span>',
  487. )
  488. foo()
  489. await nextTick()
  490. expect(container.innerHTML).toBe(
  491. '<span data-allow-mismatch=""><!--v-if--></span>',
  492. )
  493. })
  494. test('Teleport unmount (full integration)', async () => {
  495. const Comp1 = {
  496. template: `
  497. <Teleport to="#target">
  498. <span>Teleported Comp1</span>
  499. </Teleport>
  500. `,
  501. }
  502. const Comp2 = {
  503. template: `
  504. <div>Comp2</div>
  505. `,
  506. }
  507. const toggle = ref(true)
  508. const App = {
  509. template: `
  510. <div>
  511. <Comp1 v-if="toggle"/>
  512. <Comp2 v-else/>
  513. </div>
  514. `,
  515. components: {
  516. Comp1,
  517. Comp2,
  518. },
  519. setup() {
  520. return { toggle }
  521. },
  522. }
  523. const container = document.createElement('div')
  524. const teleportContainer = document.createElement('div')
  525. teleportContainer.id = 'target'
  526. document.body.appendChild(teleportContainer)
  527. // server render
  528. const ctx: SSRContext = {}
  529. container.innerHTML = await renderToString(h(App), ctx)
  530. expect(container.innerHTML).toBe(
  531. '<div><!--teleport start--><!--teleport end--></div>',
  532. )
  533. teleportContainer.innerHTML = ctx.teleports!['#target']
  534. // hydrate
  535. createSSRApp(App).mount(container)
  536. expect(container.innerHTML).toBe(
  537. '<div><!--teleport start--><!--teleport end--></div>',
  538. )
  539. expect(teleportContainer.innerHTML).toBe(
  540. '<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
  541. )
  542. expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  543. toggle.value = false
  544. await nextTick()
  545. expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
  546. expect(teleportContainer.innerHTML).toBe('')
  547. })
  548. test('Teleport unmount (mismatch + full integration)', async () => {
  549. const Comp1 = {
  550. template: `
  551. <Teleport to="#target">
  552. <span>Teleported Comp1</span>
  553. </Teleport>
  554. `,
  555. }
  556. const Comp2 = {
  557. template: `
  558. <div>Comp2</div>
  559. `,
  560. }
  561. const toggle = ref(true)
  562. const App = {
  563. template: `
  564. <div>
  565. <Comp1 v-if="toggle"/>
  566. <Comp2 v-else/>
  567. </div>
  568. `,
  569. components: {
  570. Comp1,
  571. Comp2,
  572. },
  573. setup() {
  574. return { toggle }
  575. },
  576. }
  577. const container = document.createElement('div')
  578. const teleportContainer = document.createElement('div')
  579. teleportContainer.id = 'target'
  580. document.body.appendChild(teleportContainer)
  581. // server render
  582. container.innerHTML = await renderToString(h(App))
  583. expect(container.innerHTML).toBe(
  584. '<div><!--teleport start--><!--teleport end--></div>',
  585. )
  586. expect(teleportContainer.innerHTML).toBe('')
  587. // hydrate
  588. createSSRApp(App).mount(container)
  589. expect(container.innerHTML).toBe(
  590. '<div><!--teleport start--><!--teleport end--></div>',
  591. )
  592. expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
  593. expect(`Hydration children mismatch`).toHaveBeenWarned()
  594. toggle.value = false
  595. await nextTick()
  596. expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
  597. expect(teleportContainer.innerHTML).toBe('')
  598. })
  599. test('Teleport unmount (disabled + full integration)', async () => {
  600. const disabled = ref(true)
  601. const target = ref('#teleport001')
  602. const toggle = ref(true)
  603. const Comp = {
  604. template: `
  605. <div>
  606. <div id="teleport001">
  607. <Teleport
  608. :to="target"
  609. :disabled="disabled"
  610. >
  611. <template v-for="section in order">
  612. <div>{{section}}</div>
  613. </template>
  614. </Teleport>
  615. </div>
  616. <div id="teleport002"></div>
  617. </div>
  618. `,
  619. setup() {
  620. const order = ref(['A', 'B', 'C'])
  621. return { target, disabled, order }
  622. },
  623. }
  624. const App = {
  625. template: `<Comp v-if="toggle"/>`,
  626. components: {
  627. Comp,
  628. },
  629. setup() {
  630. return { toggle }
  631. },
  632. }
  633. const container = document.createElement('div')
  634. document.body.appendChild(container)
  635. // server render
  636. container.innerHTML = await renderToString(h(App))
  637. expect(container.innerHTML).toBe(
  638. `<div>` +
  639. `<div id="teleport001">` +
  640. `<!--teleport start-->` +
  641. `<!--[--><div>A</div><div>B</div><div>C</div><!--]-->` +
  642. `<!--teleport end-->` +
  643. `</div>` +
  644. `<div id="teleport002"></div>` +
  645. `</div>`,
  646. )
  647. // hydrate
  648. createSSRApp(App).mount(container)
  649. expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  650. target.value = '#teleport002'
  651. disabled.value = false
  652. await nextTick()
  653. expect(container.querySelector('#teleport001')!.innerHTML).toBe(
  654. '<!--teleport start--><!--teleport end-->',
  655. )
  656. expect(container.querySelector('#teleport002')!.innerHTML).toBe(
  657. '<!--[--><div>A</div><div>B</div><div>C</div><!--]-->',
  658. )
  659. toggle.value = false
  660. await nextTick()
  661. expect(container.innerHTML).toBe('<!--v-if-->')
  662. })
  663. test('Teleport target change (mismatch + full integration)', async () => {
  664. const target = ref('#target1')
  665. const Comp = {
  666. template: `
  667. <Teleport :to="target">
  668. <span>Teleported</span>
  669. </Teleport>
  670. `,
  671. setup() {
  672. return { target }
  673. },
  674. }
  675. const App = {
  676. template: `
  677. <div>
  678. <Comp />
  679. </div>
  680. `,
  681. components: {
  682. Comp,
  683. },
  684. }
  685. const container = document.createElement('div')
  686. const teleportContainer1 = document.createElement('div')
  687. teleportContainer1.id = 'target1'
  688. const teleportContainer2 = document.createElement('div')
  689. teleportContainer2.id = 'target2'
  690. document.body.appendChild(teleportContainer1)
  691. document.body.appendChild(teleportContainer2)
  692. // server render
  693. container.innerHTML = await renderToString(h(App))
  694. expect(container.innerHTML).toBe(
  695. '<div><!--teleport start--><!--teleport end--></div>',
  696. )
  697. expect(teleportContainer1.innerHTML).toBe('')
  698. expect(teleportContainer2.innerHTML).toBe('')
  699. // hydrate
  700. createSSRApp(App).mount(container)
  701. expect(container.innerHTML).toBe(
  702. '<div><!--teleport start--><!--teleport end--></div>',
  703. )
  704. expect(teleportContainer1.innerHTML).toBe('<span>Teleported</span>')
  705. expect(teleportContainer2.innerHTML).toBe('')
  706. expect(`Hydration children mismatch`).toHaveBeenWarned()
  707. target.value = '#target2'
  708. await nextTick()
  709. expect(teleportContainer1.innerHTML).toBe('')
  710. expect(teleportContainer2.innerHTML).toBe('<span>Teleported</span>')
  711. })
  712. // compile SSR + client render fn from the same template & hydrate
  713. test('full compiler integration', async () => {
  714. const mounted: string[] = []
  715. const log = vi.fn()
  716. const toggle = ref(true)
  717. const Child = {
  718. data() {
  719. return {
  720. count: 0,
  721. text: 'hello',
  722. style: {
  723. color: 'red',
  724. },
  725. }
  726. },
  727. mounted() {
  728. mounted.push('child')
  729. },
  730. template: `
  731. <div>
  732. <span class="count" :style="style">{{ count }}</span>
  733. <button class="inc" @click="count++">inc</button>
  734. <button class="change" @click="style.color = 'green'" >change color</button>
  735. <button class="emit" @click="$emit('foo')">emit</button>
  736. <span class="text">{{ text }}</span>
  737. <input v-model="text">
  738. </div>
  739. `,
  740. }
  741. const App = {
  742. setup() {
  743. return { toggle }
  744. },
  745. mounted() {
  746. mounted.push('parent')
  747. },
  748. template: `
  749. <div>
  750. <span>hello</span>
  751. <template v-if="toggle">
  752. <Child @foo="log('child')"/>
  753. <template v-if="true">
  754. <button class="parent-click" @click="log('click')">click me</button>
  755. </template>
  756. </template>
  757. <span>hello</span>
  758. </div>`,
  759. components: {
  760. Child,
  761. },
  762. methods: {
  763. log,
  764. },
  765. }
  766. const container = document.createElement('div')
  767. // server render
  768. container.innerHTML = await renderToString(h(App))
  769. // hydrate
  770. createSSRApp(App).mount(container)
  771. // assert interactions
  772. // 1. parent button click
  773. triggerEvent('click', container.querySelector('.parent-click')!)
  774. expect(log).toHaveBeenCalledWith('click')
  775. // 2. child inc click + text interpolation
  776. const count = container.querySelector('.count') as HTMLElement
  777. expect(count.textContent).toBe(`0`)
  778. triggerEvent('click', container.querySelector('.inc')!)
  779. await nextTick()
  780. expect(count.textContent).toBe(`1`)
  781. // 3. child color click + style binding
  782. expect(count.style.color).toBe('red')
  783. triggerEvent('click', container.querySelector('.change')!)
  784. await nextTick()
  785. expect(count.style.color).toBe('green')
  786. // 4. child event emit
  787. triggerEvent('click', container.querySelector('.emit')!)
  788. expect(log).toHaveBeenCalledWith('child')
  789. // 5. child v-model
  790. const text = container.querySelector('.text')!
  791. const input = container.querySelector('input')!
  792. expect(text.textContent).toBe('hello')
  793. input.value = 'bye'
  794. triggerEvent('input', input)
  795. await nextTick()
  796. expect(text.textContent).toBe('bye')
  797. })
  798. test('handle click error in ssr mode', async () => {
  799. const App = {
  800. setup() {
  801. const throwError = () => {
  802. throw new Error('Sentry Error')
  803. }
  804. return { throwError }
  805. },
  806. template: `
  807. <div>
  808. <button class="parent-click" @click="throwError">click me</button>
  809. </div>`,
  810. }
  811. const container = document.createElement('div')
  812. // server render
  813. container.innerHTML = await renderToString(h(App))
  814. // hydrate
  815. const app = createSSRApp(App)
  816. const handler = (app.config.errorHandler = vi.fn())
  817. app.mount(container)
  818. // assert interactions
  819. // parent button click
  820. triggerEvent('click', container.querySelector('.parent-click')!)
  821. expect(handler).toHaveBeenCalled()
  822. })
  823. test('handle blur error in ssr mode', async () => {
  824. const App = {
  825. setup() {
  826. const throwError = () => {
  827. throw new Error('Sentry Error')
  828. }
  829. return { throwError }
  830. },
  831. template: `
  832. <div>
  833. <input class="parent-click" @blur="throwError"/>
  834. </div>`,
  835. }
  836. const container = document.createElement('div')
  837. // server render
  838. container.innerHTML = await renderToString(h(App))
  839. // hydrate
  840. const app = createSSRApp(App)
  841. const handler = (app.config.errorHandler = vi.fn())
  842. app.mount(container)
  843. // assert interactions
  844. // parent blur event
  845. triggerEvent('blur', container.querySelector('.parent-click')!)
  846. expect(handler).toHaveBeenCalled()
  847. })
  848. test('Suspense', async () => {
  849. const AsyncChild = {
  850. async setup() {
  851. const count = ref(0)
  852. return () =>
  853. h(
  854. 'span',
  855. {
  856. onClick: () => {
  857. count.value++
  858. },
  859. },
  860. count.value,
  861. )
  862. },
  863. }
  864. const { vnode, container } = mountWithHydration('<span>0</span>', () =>
  865. h(Suspense, () => h(AsyncChild)),
  866. )
  867. expect(vnode.el).toBe(container.firstChild)
  868. // wait for hydration to finish
  869. await new Promise(r => setTimeout(r))
  870. triggerEvent('click', container.querySelector('span')!)
  871. await nextTick()
  872. expect(container.innerHTML).toBe(`<span>1</span>`)
  873. })
  874. // #6638
  875. test('Suspense + async component', async () => {
  876. let isSuspenseResolved = false
  877. let isSuspenseResolvedInChild: any
  878. const AsyncChild = defineAsyncComponent(() =>
  879. Promise.resolve(
  880. defineComponent({
  881. setup() {
  882. isSuspenseResolvedInChild = isSuspenseResolved
  883. const count = ref(0)
  884. return () =>
  885. h(
  886. 'span',
  887. {
  888. onClick: () => {
  889. count.value++
  890. },
  891. },
  892. count.value,
  893. )
  894. },
  895. }),
  896. ),
  897. )
  898. const { vnode, container } = mountWithHydration('<span>0</span>', () =>
  899. h(
  900. Suspense,
  901. {
  902. onResolve() {
  903. isSuspenseResolved = true
  904. },
  905. },
  906. () => h(AsyncChild),
  907. ),
  908. )
  909. expect(vnode.el).toBe(container.firstChild)
  910. // wait for hydration to finish
  911. await new Promise(r => setTimeout(r))
  912. expect(isSuspenseResolvedInChild).toBe(false)
  913. expect(isSuspenseResolved).toBe(true)
  914. // assert interaction
  915. triggerEvent('click', container.querySelector('span')!)
  916. await nextTick()
  917. expect(container.innerHTML).toBe(`<span>1</span>`)
  918. })
  919. test('Suspense (full integration)', async () => {
  920. const mountedCalls: number[] = []
  921. const asyncDeps: Promise<any>[] = []
  922. const AsyncChild = defineComponent({
  923. props: ['n'],
  924. async setup(props) {
  925. const count = ref(props.n)
  926. onMounted(() => {
  927. mountedCalls.push(props.n)
  928. })
  929. const p = new Promise(r => setTimeout(r, props.n * 10))
  930. asyncDeps.push(p)
  931. await p
  932. return () =>
  933. h(
  934. 'span',
  935. {
  936. onClick: () => {
  937. count.value++
  938. },
  939. },
  940. count.value,
  941. )
  942. },
  943. })
  944. const done = vi.fn()
  945. const App = {
  946. template: `
  947. <Suspense @resolve="done">
  948. <div>
  949. <AsyncChild :n="1" />
  950. <AsyncChild :n="2" />
  951. </div>
  952. </Suspense>`,
  953. components: {
  954. AsyncChild,
  955. },
  956. methods: {
  957. done,
  958. },
  959. }
  960. const container = document.createElement('div')
  961. // server render
  962. container.innerHTML = await renderToString(h(App))
  963. expect(container.innerHTML).toMatchInlineSnapshot(
  964. `"<div><span>1</span><span>2</span></div>"`,
  965. )
  966. // reset asyncDeps from ssr
  967. asyncDeps.length = 0
  968. // hydrate
  969. createSSRApp(App).mount(container)
  970. expect(mountedCalls.length).toBe(0)
  971. expect(asyncDeps.length).toBe(2)
  972. // wait for hydration to complete
  973. await Promise.all(asyncDeps)
  974. await new Promise(r => setTimeout(r))
  975. // should flush buffered effects
  976. expect(mountedCalls).toMatchObject([1, 2])
  977. expect(container.innerHTML).toMatch(
  978. `<div><span>1</span><span>2</span></div>`,
  979. )
  980. const span1 = container.querySelector('span')!
  981. triggerEvent('click', span1)
  982. await nextTick()
  983. expect(container.innerHTML).toMatch(
  984. `<div><span>2</span><span>2</span></div>`,
  985. )
  986. const span2 = span1.nextSibling as Element
  987. triggerEvent('click', span2)
  988. await nextTick()
  989. expect(container.innerHTML).toMatch(
  990. `<div><span>2</span><span>3</span></div>`,
  991. )
  992. })
  993. test('async component', async () => {
  994. const spy = vi.fn()
  995. const Comp = () =>
  996. h(
  997. 'button',
  998. {
  999. onClick: spy,
  1000. },
  1001. 'hello!',
  1002. )
  1003. let serverResolve: any
  1004. let AsyncComp = defineAsyncComponent(
  1005. () =>
  1006. new Promise(r => {
  1007. serverResolve = r
  1008. }),
  1009. )
  1010. const App = {
  1011. render() {
  1012. return ['hello', h(AsyncComp), 'world']
  1013. },
  1014. }
  1015. // server render
  1016. const htmlPromise = renderToString(h(App))
  1017. serverResolve(Comp)
  1018. const html = await htmlPromise
  1019. expect(html).toMatchInlineSnapshot(
  1020. `"<!--[-->hello<button>hello!</button>world<!--]-->"`,
  1021. )
  1022. // hydration
  1023. let clientResolve: any
  1024. AsyncComp = defineAsyncComponent(
  1025. () =>
  1026. new Promise(r => {
  1027. clientResolve = r
  1028. }),
  1029. )
  1030. const container = document.createElement('div')
  1031. container.innerHTML = html
  1032. createSSRApp(App).mount(container)
  1033. // hydration not complete yet
  1034. triggerEvent('click', container.querySelector('button')!)
  1035. expect(spy).not.toHaveBeenCalled()
  1036. // resolve
  1037. clientResolve(Comp)
  1038. await new Promise(r => setTimeout(r))
  1039. // should be hydrated now
  1040. triggerEvent('click', container.querySelector('button')!)
  1041. expect(spy).toHaveBeenCalled()
  1042. })
  1043. test('update async wrapper before resolve', async () => {
  1044. const Comp = {
  1045. render() {
  1046. return h('h1', 'Async component')
  1047. },
  1048. }
  1049. let serverResolve: any
  1050. let AsyncComp = defineAsyncComponent(
  1051. () =>
  1052. new Promise(r => {
  1053. serverResolve = r
  1054. }),
  1055. )
  1056. const toggle = ref(true)
  1057. const App = {
  1058. setup() {
  1059. onMounted(() => {
  1060. // change state, this makes updateComponent(AsyncComp) execute before
  1061. // the async component is resolved
  1062. toggle.value = false
  1063. })
  1064. return () => {
  1065. return [toggle.value ? 'hello' : 'world', h(AsyncComp)]
  1066. }
  1067. },
  1068. }
  1069. // server render
  1070. const htmlPromise = renderToString(h(App))
  1071. serverResolve(Comp)
  1072. const html = await htmlPromise
  1073. expect(html).toMatchInlineSnapshot(
  1074. `"<!--[-->hello<h1>Async component</h1><!--]-->"`,
  1075. )
  1076. // hydration
  1077. let clientResolve: any
  1078. AsyncComp = defineAsyncComponent(
  1079. () =>
  1080. new Promise(r => {
  1081. clientResolve = r
  1082. }),
  1083. )
  1084. const container = document.createElement('div')
  1085. container.innerHTML = html
  1086. createSSRApp(App).mount(container)
  1087. // resolve
  1088. clientResolve(Comp)
  1089. await new Promise(r => setTimeout(r))
  1090. // should be hydrated now
  1091. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  1092. expect(container.innerHTML).toMatchInlineSnapshot(
  1093. `"<!--[-->world<h1>Async component</h1><!--]-->"`,
  1094. )
  1095. })
  1096. // #13510
  1097. test('update async component after parent mount before async component resolve', async () => {
  1098. const Comp = {
  1099. props: ['toggle'],
  1100. render(this: any) {
  1101. return h('h1', [
  1102. this.toggle ? 'Async component' : 'Updated async component',
  1103. ])
  1104. },
  1105. }
  1106. let serverResolve: any
  1107. let AsyncComp = defineAsyncComponent(
  1108. () =>
  1109. new Promise(r => {
  1110. serverResolve = r
  1111. }),
  1112. )
  1113. const toggle = ref(true)
  1114. const App = {
  1115. setup() {
  1116. onMounted(() => {
  1117. // change state, after mount and before async component resolve
  1118. nextTick(() => (toggle.value = false))
  1119. })
  1120. return () => {
  1121. return h(AsyncComp, { toggle: toggle.value })
  1122. }
  1123. },
  1124. }
  1125. // server render
  1126. const htmlPromise = renderToString(h(App))
  1127. serverResolve(Comp)
  1128. const html = await htmlPromise
  1129. expect(html).toMatchInlineSnapshot(`"<h1>Async component</h1>"`)
  1130. // hydration
  1131. let clientResolve: any
  1132. AsyncComp = defineAsyncComponent(
  1133. () =>
  1134. new Promise(r => {
  1135. clientResolve = r
  1136. }),
  1137. )
  1138. const container = document.createElement('div')
  1139. container.innerHTML = html
  1140. createSSRApp(App).mount(container)
  1141. // resolve
  1142. clientResolve(Comp)
  1143. await new Promise(r => setTimeout(r))
  1144. // prevent lazy hydration since the component has been patched
  1145. expect('Skipping lazy hydration for component').toHaveBeenWarned()
  1146. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  1147. expect(container.innerHTML).toMatchInlineSnapshot(
  1148. `"<h1>Updated async component</h1>"`,
  1149. )
  1150. })
  1151. test('hydrate safely when property used by async setup changed before render', async () => {
  1152. const toggle = ref(true)
  1153. const AsyncComp = {
  1154. async setup() {
  1155. await new Promise<void>(r => setTimeout(r, 10))
  1156. return () => h('h1', 'Async component')
  1157. },
  1158. }
  1159. const AsyncWrapper = {
  1160. render() {
  1161. return h(AsyncComp)
  1162. },
  1163. }
  1164. const SiblingComp = {
  1165. setup() {
  1166. toggle.value = false
  1167. return () => h('span')
  1168. },
  1169. }
  1170. const App = {
  1171. setup() {
  1172. return () =>
  1173. h(
  1174. Suspense,
  1175. {},
  1176. {
  1177. default: () => [
  1178. h('main', {}, [
  1179. h(AsyncWrapper, {
  1180. prop: toggle.value ? 'hello' : 'world',
  1181. }),
  1182. h(SiblingComp),
  1183. ]),
  1184. ],
  1185. },
  1186. )
  1187. },
  1188. }
  1189. // server render
  1190. const html = await renderToString(h(App))
  1191. expect(html).toMatchInlineSnapshot(
  1192. `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
  1193. )
  1194. expect(toggle.value).toBe(false)
  1195. // hydration
  1196. // reset the value
  1197. toggle.value = true
  1198. expect(toggle.value).toBe(true)
  1199. const container = document.createElement('div')
  1200. container.innerHTML = html
  1201. createSSRApp(App).mount(container)
  1202. await new Promise(r => setTimeout(r, 10))
  1203. expect(toggle.value).toBe(false)
  1204. // should be hydrated now
  1205. expect(container.innerHTML).toMatchInlineSnapshot(
  1206. `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
  1207. )
  1208. })
  1209. test('hydrate safely when property used by deep nested async setup changed before render', async () => {
  1210. const toggle = ref(true)
  1211. const AsyncComp = {
  1212. async setup() {
  1213. await new Promise<void>(r => setTimeout(r, 10))
  1214. return () => h('h1', 'Async component')
  1215. },
  1216. }
  1217. const AsyncWrapper = { render: () => h(AsyncComp) }
  1218. const AsyncWrapperWrapper = { render: () => h(AsyncWrapper) }
  1219. const SiblingComp = {
  1220. setup() {
  1221. toggle.value = false
  1222. return () => h('span')
  1223. },
  1224. }
  1225. const App = {
  1226. setup() {
  1227. return () =>
  1228. h(
  1229. Suspense,
  1230. {},
  1231. {
  1232. default: () => [
  1233. h('main', {}, [
  1234. h(AsyncWrapperWrapper, {
  1235. prop: toggle.value ? 'hello' : 'world',
  1236. }),
  1237. h(SiblingComp),
  1238. ]),
  1239. ],
  1240. },
  1241. )
  1242. },
  1243. }
  1244. // server render
  1245. const html = await renderToString(h(App))
  1246. expect(html).toMatchInlineSnapshot(
  1247. `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
  1248. )
  1249. expect(toggle.value).toBe(false)
  1250. // hydration
  1251. // reset the value
  1252. toggle.value = true
  1253. expect(toggle.value).toBe(true)
  1254. const container = document.createElement('div')
  1255. container.innerHTML = html
  1256. createSSRApp(App).mount(container)
  1257. await new Promise(r => setTimeout(r, 10))
  1258. expect(toggle.value).toBe(false)
  1259. // should be hydrated now
  1260. expect(container.innerHTML).toMatchInlineSnapshot(
  1261. `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
  1262. )
  1263. })
  1264. // #3787
  1265. test('unmount async wrapper before load', async () => {
  1266. let resolve: any
  1267. const AsyncComp = defineAsyncComponent(
  1268. () =>
  1269. new Promise(r => {
  1270. resolve = r
  1271. }),
  1272. )
  1273. const show = ref(true)
  1274. const root = document.createElement('div')
  1275. root.innerHTML = '<div><div>async</div></div>'
  1276. createSSRApp({
  1277. render() {
  1278. return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
  1279. },
  1280. }).mount(root)
  1281. show.value = false
  1282. await nextTick()
  1283. expect(root.innerHTML).toBe('<div><div>hi</div></div>')
  1284. resolve({})
  1285. })
  1286. //#12362
  1287. test('nested async wrapper', async () => {
  1288. const Toggle = defineAsyncComponent(
  1289. () =>
  1290. new Promise(r => {
  1291. r(
  1292. defineComponent({
  1293. setup(_, { slots }) {
  1294. const show = ref(false)
  1295. onMounted(() => {
  1296. nextTick(() => {
  1297. show.value = true
  1298. })
  1299. })
  1300. return () =>
  1301. withDirectives(
  1302. h('div', null, [renderSlot(slots, 'default')]),
  1303. [[vShow, show.value]],
  1304. )
  1305. },
  1306. }) as any,
  1307. )
  1308. }),
  1309. )
  1310. const Wrapper = defineAsyncComponent(() => {
  1311. return new Promise(r => {
  1312. r(
  1313. defineComponent({
  1314. render(this: any) {
  1315. return renderSlot(this.$slots, 'default')
  1316. },
  1317. }) as any,
  1318. )
  1319. })
  1320. })
  1321. const count = ref(0)
  1322. const fn = vi.fn()
  1323. const Child = {
  1324. setup() {
  1325. onMounted(() => {
  1326. fn()
  1327. count.value++
  1328. })
  1329. return () => h('div', count.value)
  1330. },
  1331. }
  1332. const App = {
  1333. render() {
  1334. return h(Toggle, null, {
  1335. default: () =>
  1336. h(Wrapper, null, {
  1337. default: () =>
  1338. h(Wrapper, null, {
  1339. default: () => h(Child),
  1340. }),
  1341. }),
  1342. })
  1343. },
  1344. }
  1345. const root = document.createElement('div')
  1346. root.innerHTML = await renderToString(h(App))
  1347. expect(root.innerHTML).toMatchInlineSnapshot(
  1348. `"<div style="display:none;"><!--[--><!--[--><!--[--><div>0</div><!--]--><!--]--><!--]--></div>"`,
  1349. )
  1350. createSSRApp(App).mount(root)
  1351. await nextTick()
  1352. await nextTick()
  1353. expect(root.innerHTML).toMatchInlineSnapshot(
  1354. `"<div style=""><!--[--><!--[--><!--[--><div>1</div><!--]--><!--]--><!--]--></div>"`,
  1355. )
  1356. expect(fn).toBeCalledTimes(1)
  1357. })
  1358. test('unmount async wrapper before load (fragment)', async () => {
  1359. let resolve: any
  1360. const AsyncComp = defineAsyncComponent(
  1361. () =>
  1362. new Promise(r => {
  1363. resolve = r
  1364. }),
  1365. )
  1366. const show = ref(true)
  1367. const root = document.createElement('div')
  1368. root.innerHTML = '<div><!--[-->async<!--]--></div>'
  1369. createSSRApp({
  1370. render() {
  1371. return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
  1372. },
  1373. }).mount(root)
  1374. show.value = false
  1375. await nextTick()
  1376. expect(root.innerHTML).toBe('<div><div>hi</div></div>')
  1377. resolve({})
  1378. })
  1379. test('elements with camel-case in svg ', () => {
  1380. const { vnode, container } = mountWithHydration(
  1381. '<animateTransform></animateTransform>',
  1382. () => h('animateTransform'),
  1383. )
  1384. expect(vnode.el).toBe(container.firstChild)
  1385. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  1386. })
  1387. test('SVG as a mount container', () => {
  1388. const svgContainer = document.createElement('svg')
  1389. svgContainer.innerHTML = '<g></g>'
  1390. const app = createSSRApp({
  1391. render: () => h('g'),
  1392. })
  1393. expect(
  1394. (
  1395. app.mount(svgContainer).$.subTree as VNode<Node, Element> & {
  1396. el: Element
  1397. }
  1398. ).el instanceof SVGElement,
  1399. )
  1400. })
  1401. test('force hydrate prop with `.prop` modifier', () => {
  1402. const { container } = mountWithHydration('<input type="checkbox">', () =>
  1403. h('input', {
  1404. type: 'checkbox',
  1405. '.indeterminate': true,
  1406. }),
  1407. )
  1408. expect((container.firstChild! as any).indeterminate).toBe(true)
  1409. })
  1410. test('force hydrate input v-model with non-string value bindings', () => {
  1411. const { container } = mountWithHydration(
  1412. '<input type="checkbox" value="true">',
  1413. () =>
  1414. withDirectives(
  1415. createVNode(
  1416. 'input',
  1417. { type: 'checkbox', 'true-value': true },
  1418. null,
  1419. PatchFlags.PROPS,
  1420. ['true-value'],
  1421. ),
  1422. [[vModelCheckbox, true]],
  1423. ),
  1424. )
  1425. expect((container.firstChild as any)._trueValue).toBe(true)
  1426. })
  1427. test('force hydrate checkbox with indeterminate', () => {
  1428. const { container } = mountWithHydration(
  1429. '<input type="checkbox" indeterminate>',
  1430. () =>
  1431. createVNode(
  1432. 'input',
  1433. { type: 'checkbox', indeterminate: '' },
  1434. null,
  1435. PatchFlags.CACHED,
  1436. ),
  1437. )
  1438. expect((container.firstChild as any).indeterminate).toBe(true)
  1439. })
  1440. test('force hydrate select option with non-string value bindings', () => {
  1441. const { container } = mountWithHydration(
  1442. '<select><option value="true">ok</option></select>',
  1443. () =>
  1444. h('select', [
  1445. // hoisted because bound value is a constant...
  1446. createVNode('option', { value: true }, null, -1 /* HOISTED */),
  1447. ]),
  1448. )
  1449. expect((container.firstChild!.firstChild as any)._value).toBe(true)
  1450. })
  1451. // #7203
  1452. test('force hydrate custom element with dynamic props', () => {
  1453. class MyElement extends HTMLElement {
  1454. foo = ''
  1455. constructor() {
  1456. super()
  1457. }
  1458. }
  1459. customElements.define('my-element-7203', MyElement)
  1460. const msg = ref('bar')
  1461. const container = document.createElement('div')
  1462. container.innerHTML = '<my-element-7203></my-element-7203>'
  1463. const app = createSSRApp({
  1464. render: () => h('my-element-7203', { foo: msg.value }),
  1465. })
  1466. app.mount(container)
  1467. expect((container.firstChild as any).foo).toBe(msg.value)
  1468. })
  1469. // #14274
  1470. test('should not render ref on custom element during hydration', () => {
  1471. const container = document.createElement('div')
  1472. container.innerHTML = '<my-element>hello</my-element>'
  1473. const root = ref()
  1474. const app = createSSRApp({
  1475. render: () =>
  1476. h('my-element', {
  1477. ref: root,
  1478. innerHTML: 'hello',
  1479. }),
  1480. })
  1481. app.mount(container)
  1482. expect(container.innerHTML).toBe('<my-element>hello</my-element>')
  1483. expect((container.firstChild as Element).hasAttribute('ref')).toBe(false)
  1484. expect(root.value).toBe(container.firstChild)
  1485. })
  1486. // #5728
  1487. test('empty text node in slot', () => {
  1488. const Comp = {
  1489. render(this: any) {
  1490. return renderSlot(this.$slots, 'default', {}, () => [
  1491. createTextVNode(''),
  1492. ])
  1493. },
  1494. }
  1495. const { container, vnode } = mountWithHydration('<!--[--><!--]-->', () =>
  1496. h(Comp),
  1497. )
  1498. expect(container.childNodes.length).toBe(3)
  1499. const text = container.childNodes[1]
  1500. expect(text.nodeType).toBe(3)
  1501. expect(vnode.el).toBe(container.childNodes[0])
  1502. // component => slot fragment => text node
  1503. expect((vnode as any).component?.subTree.children[0].el).toBe(text)
  1504. })
  1505. // #7215
  1506. test('empty text node', () => {
  1507. const Comp = {
  1508. render(this: any) {
  1509. return h('p', [''])
  1510. },
  1511. }
  1512. const { container } = mountWithHydration('<p></p>', () => h(Comp))
  1513. expect(container.childNodes.length).toBe(1)
  1514. const p = container.childNodes[0]
  1515. expect(p.childNodes.length).toBe(1)
  1516. const text = p.childNodes[0]
  1517. expect(text.nodeType).toBe(3)
  1518. })
  1519. // #11372
  1520. test('object style value tracking in prod', async () => {
  1521. __DEV__ = false
  1522. try {
  1523. const style = reactive({ color: 'red' })
  1524. const Comp = {
  1525. render(this: any) {
  1526. return (
  1527. openBlock(),
  1528. createElementBlock(
  1529. 'div',
  1530. {
  1531. style: normalizeStyle(style),
  1532. },
  1533. null,
  1534. 4 /* STYLE */,
  1535. )
  1536. )
  1537. },
  1538. }
  1539. const { container } = mountWithHydration(
  1540. `<div style="color: red;"></div>`,
  1541. () => h(Comp),
  1542. )
  1543. style.color = 'green'
  1544. await nextTick()
  1545. expect(container.innerHTML).toBe(`<div style="color: green;"></div>`)
  1546. } finally {
  1547. __DEV__ = true
  1548. }
  1549. })
  1550. test('app.unmount()', async () => {
  1551. const container = document.createElement('DIV')
  1552. container.innerHTML = '<button></button>'
  1553. const App = defineComponent({
  1554. setup(_, { expose }) {
  1555. const count = ref(0)
  1556. expose({ count })
  1557. return () =>
  1558. h('button', {
  1559. onClick: () => count.value++,
  1560. })
  1561. },
  1562. })
  1563. const app = createSSRApp(App)
  1564. const vm = app.mount(container)
  1565. await nextTick()
  1566. expect((container as any)._vnode).toBeDefined()
  1567. // @ts-expect-error - expose()'d properties are not available on vm type
  1568. expect(vm.count).toBe(0)
  1569. app.unmount()
  1570. expect((container as any)._vnode).toBe(null)
  1571. })
  1572. // #6637
  1573. test('stringified root fragment', () => {
  1574. mountWithHydration(`<!--[--><div></div><!--]-->`, () =>
  1575. createStaticVNode(`<div></div>`, 1),
  1576. )
  1577. expect(`mismatch`).not.toHaveBeenWarned()
  1578. })
  1579. test('transition appear', () => {
  1580. const { vnode, container } = mountWithHydration(
  1581. `<template><div>foo</div></template>`,
  1582. () =>
  1583. h(
  1584. Transition,
  1585. { appear: true },
  1586. {
  1587. default: () => h('div', 'foo'),
  1588. },
  1589. ),
  1590. )
  1591. expect(container.firstChild).toMatchInlineSnapshot(`
  1592. <div
  1593. class="v-enter-from v-enter-active"
  1594. >
  1595. foo
  1596. </div>
  1597. `)
  1598. expect(vnode.el).toBe(container.firstChild)
  1599. expect(`mismatch`).not.toHaveBeenWarned()
  1600. })
  1601. test('transition appear work with pre-existing class', () => {
  1602. const { vnode, container } = mountWithHydration(
  1603. `<template><div class="foo">foo</div></template>`,
  1604. () =>
  1605. h(
  1606. Transition,
  1607. { appear: true },
  1608. {
  1609. default: () => h('div', { class: 'foo' }, 'foo'),
  1610. },
  1611. ),
  1612. )
  1613. expect(container.firstChild).toMatchInlineSnapshot(`
  1614. <div
  1615. class="foo v-enter-from v-enter-active"
  1616. >
  1617. foo
  1618. </div>
  1619. `)
  1620. expect(vnode.el).toBe(container.firstChild)
  1621. expect(`mismatch`).not.toHaveBeenWarned()
  1622. })
  1623. // #13394
  1624. test('transition appear work with empty content', async () => {
  1625. const show = ref(true)
  1626. const { vnode, container } = mountWithHydration(
  1627. `<template><!----></template>`,
  1628. function (this: any) {
  1629. return h(
  1630. Transition,
  1631. { appear: true },
  1632. {
  1633. default: () =>
  1634. show.value
  1635. ? renderSlot(this.$slots, 'default')
  1636. : createTextVNode('foo'),
  1637. },
  1638. )
  1639. },
  1640. )
  1641. // empty slot render as a comment node
  1642. expect(container.firstChild!.nodeType).toBe(Node.COMMENT_NODE)
  1643. expect(vnode.el).toBe(container.firstChild)
  1644. expect(`mismatch`).not.toHaveBeenWarned()
  1645. show.value = false
  1646. await nextTick()
  1647. expect(container.innerHTML).toBe('foo')
  1648. })
  1649. test('transition appear with v-if', () => {
  1650. const show = false
  1651. const { vnode, container } = mountWithHydration(
  1652. `<template><!----></template>`,
  1653. () =>
  1654. h(
  1655. Transition,
  1656. { appear: true },
  1657. {
  1658. default: () => (show ? h('div', 'foo') : createCommentVNode('')),
  1659. },
  1660. ),
  1661. )
  1662. expect(container.firstChild).toMatchInlineSnapshot('<!---->')
  1663. expect(vnode.el).toBe(container.firstChild)
  1664. expect(`mismatch`).not.toHaveBeenWarned()
  1665. })
  1666. test('transition appear with v-show', () => {
  1667. const show = false
  1668. const { vnode, container } = mountWithHydration(
  1669. `<template><div style="display: none;">foo</div></template>`,
  1670. () =>
  1671. h(
  1672. Transition,
  1673. { appear: true },
  1674. {
  1675. default: () =>
  1676. withDirectives(createVNode('div', null, 'foo'), [[vShow, show]]),
  1677. },
  1678. ),
  1679. )
  1680. expect(container.firstChild).toMatchInlineSnapshot(`
  1681. <div
  1682. class="v-enter-from v-enter-active"
  1683. style="display: none;"
  1684. >
  1685. foo
  1686. </div>
  1687. `)
  1688. expect((container.firstChild as any)[vShowOriginalDisplay]).toBe('')
  1689. expect(vnode.el).toBe(container.firstChild)
  1690. expect(`mismatch`).not.toHaveBeenWarned()
  1691. })
  1692. test('transition appear w/ event listener', async () => {
  1693. const container = document.createElement('div')
  1694. container.innerHTML = `<template><button>0</button></template>`
  1695. createSSRApp({
  1696. data() {
  1697. return {
  1698. count: 0,
  1699. }
  1700. },
  1701. template: `
  1702. <Transition appear>
  1703. <button @click="count++">{{count}}</button>
  1704. </Transition>
  1705. `,
  1706. }).mount(container)
  1707. expect(container.firstChild).toMatchInlineSnapshot(`
  1708. <button
  1709. class="v-enter-from v-enter-active"
  1710. >
  1711. 0
  1712. </button>
  1713. `)
  1714. triggerEvent('click', container.querySelector('button')!)
  1715. await nextTick()
  1716. expect(container.firstChild).toMatchInlineSnapshot(`
  1717. <button
  1718. class="v-enter-from v-enter-active"
  1719. >
  1720. 1
  1721. </button>
  1722. `)
  1723. })
  1724. test('Suspense + transition appear', async () => {
  1725. const { vnode, container } = mountWithHydration(
  1726. `<template><div>foo</div></template>`,
  1727. () =>
  1728. h(Suspense, {}, () =>
  1729. h(
  1730. Transition,
  1731. { appear: true },
  1732. {
  1733. default: () => h('div', 'foo'),
  1734. },
  1735. ),
  1736. ),
  1737. )
  1738. expect(vnode.el).toBe(container.firstChild)
  1739. // wait for hydration to finish
  1740. await new Promise(r => setTimeout(r))
  1741. expect(container.firstChild).toMatchInlineSnapshot(`
  1742. <div
  1743. class="v-enter-from v-enter-active"
  1744. >
  1745. foo
  1746. </div>
  1747. `)
  1748. await nextTick()
  1749. expect(vnode.el).toBe(container.firstChild)
  1750. })
  1751. // #10607
  1752. test('update component stable slot (prod + optimized mode)', async () => {
  1753. __DEV__ = false
  1754. try {
  1755. const container = document.createElement('div')
  1756. container.innerHTML = `<template><div show="false"><!--[--><div><div><!----></div></div><div>0</div><!--]--></div></template>`
  1757. const Comp = {
  1758. render(this: any) {
  1759. return (
  1760. openBlock(),
  1761. createElementBlock('div', null, [
  1762. renderSlot(this.$slots, 'default'),
  1763. ])
  1764. )
  1765. },
  1766. }
  1767. const show = ref(false)
  1768. const clicked = ref(false)
  1769. const Wrapper = {
  1770. setup() {
  1771. const items = ref<number[]>([])
  1772. onMounted(() => {
  1773. items.value = [1]
  1774. })
  1775. return () => {
  1776. return (
  1777. openBlock(),
  1778. createBlock(Comp, null, {
  1779. default: withCtx(() => [
  1780. createElementVNode('div', null, [
  1781. createElementVNode('div', null, [
  1782. clicked.value
  1783. ? (openBlock(),
  1784. createElementBlock('div', { key: 0 }, 'foo'))
  1785. : createCommentVNode('v-if', true),
  1786. ]),
  1787. ]),
  1788. createElementVNode(
  1789. 'div',
  1790. null,
  1791. items.value.length,
  1792. 1 /* TEXT */,
  1793. ),
  1794. ]),
  1795. _: 1 /* STABLE */,
  1796. })
  1797. )
  1798. }
  1799. },
  1800. }
  1801. createSSRApp({
  1802. components: { Wrapper },
  1803. data() {
  1804. return { show }
  1805. },
  1806. template: `<Wrapper :show="show"/>`,
  1807. }).mount(container)
  1808. await nextTick()
  1809. expect(container.innerHTML).toBe(
  1810. `<div show="false"><!--[--><div><div><!----></div></div><div>1</div><!--]--></div>`,
  1811. )
  1812. show.value = true
  1813. await nextTick()
  1814. expect(async () => {
  1815. clicked.value = true
  1816. await nextTick()
  1817. }).not.toThrow("Cannot read properties of null (reading 'insertBefore')")
  1818. await nextTick()
  1819. expect(container.innerHTML).toBe(
  1820. `<div show="true"><!--[--><div><div><div>foo</div></div></div><div>1</div><!--]--></div>`,
  1821. )
  1822. } catch (e) {
  1823. throw e
  1824. } finally {
  1825. __DEV__ = true
  1826. }
  1827. })
  1828. test('hmr reload child wrapped in KeepAlive', async () => {
  1829. const id = 'child-reload'
  1830. const Child = {
  1831. __hmrId: id,
  1832. template: `<div>foo</div>`,
  1833. }
  1834. createRecord(id, Child)
  1835. const appId = 'test-app-id'
  1836. const App = {
  1837. __hmrId: appId,
  1838. components: { Child },
  1839. template: `
  1840. <div>
  1841. <KeepAlive>
  1842. <Child />
  1843. </KeepAlive>
  1844. </div>
  1845. `,
  1846. }
  1847. const root = document.createElement('div')
  1848. root.innerHTML = await renderToString(h(App))
  1849. createSSRApp(App).mount(root)
  1850. expect(root.innerHTML).toBe('<div><div>foo</div></div>')
  1851. reload(id, {
  1852. __hmrId: id,
  1853. template: `<div>bar</div>`,
  1854. })
  1855. await nextTick()
  1856. expect(root.innerHTML).toBe('<div><div>bar</div></div>')
  1857. })
  1858. test('hmr root reload', async () => {
  1859. const appId = 'test-app-id'
  1860. const App = {
  1861. __hmrId: appId,
  1862. template: `<div>foo</div>`,
  1863. }
  1864. const root = document.createElement('div')
  1865. root.innerHTML = await renderToString(h(App))
  1866. createSSRApp(App).mount(root)
  1867. expect(root.innerHTML).toBe('<div>foo</div>')
  1868. reload(appId, {
  1869. __hmrId: appId,
  1870. template: `<div>bar</div>`,
  1871. })
  1872. await nextTick()
  1873. expect(root.innerHTML).toBe('<div>bar</div>')
  1874. })
  1875. describe('mismatch handling', () => {
  1876. test('text node', () => {
  1877. const { container } = mountWithHydration(`foo`, () => 'bar')
  1878. expect(container.textContent).toBe('bar')
  1879. expect(`Hydration text mismatch`).toHaveBeenWarned()
  1880. })
  1881. test('element text content', () => {
  1882. const { container } = mountWithHydration(`<div>foo</div>`, () =>
  1883. h('div', 'bar'),
  1884. )
  1885. expect(container.innerHTML).toBe('<div>bar</div>')
  1886. expect(`Hydration text content mismatch`).toHaveBeenWarned()
  1887. })
  1888. test('not enough children', () => {
  1889. const { container } = mountWithHydration(`<div></div>`, () =>
  1890. h('div', [h('span', 'foo'), h('span', 'bar')]),
  1891. )
  1892. expect(container.innerHTML).toBe(
  1893. '<div><span>foo</span><span>bar</span></div>',
  1894. )
  1895. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1896. })
  1897. test('too many children', () => {
  1898. const { container } = mountWithHydration(
  1899. `<div><span>foo</span><span>bar</span></div>`,
  1900. () => h('div', [h('span', 'foo')]),
  1901. )
  1902. expect(container.innerHTML).toBe('<div><span>foo</span></div>')
  1903. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1904. })
  1905. test('complete mismatch', () => {
  1906. const { container } = mountWithHydration(
  1907. `<div><span>foo</span><span>bar</span></div>`,
  1908. () => h('div', [h('div', 'foo'), h('p', 'bar')]),
  1909. )
  1910. expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
  1911. expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
  1912. })
  1913. test('fragment mismatch removal', () => {
  1914. const { container } = mountWithHydration(
  1915. `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  1916. () => h('div', [h('span', 'replaced')]),
  1917. )
  1918. expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
  1919. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1920. })
  1921. test('fragment not enough children', () => {
  1922. const { container } = mountWithHydration(
  1923. `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  1924. () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
  1925. )
  1926. expect(container.innerHTML).toBe(
  1927. '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
  1928. )
  1929. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1930. })
  1931. test('fragment too many children', () => {
  1932. const { container } = mountWithHydration(
  1933. `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  1934. () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
  1935. )
  1936. expect(container.innerHTML).toBe(
  1937. '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>',
  1938. )
  1939. // fragment ends early and attempts to hydrate the extra <div>bar</div>
  1940. // as 2nd fragment child.
  1941. expect(`Hydration text content mismatch`).toHaveBeenWarned()
  1942. // excessive children removal
  1943. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1944. })
  1945. test('Teleport target has empty children', () => {
  1946. const teleportContainer = document.createElement('div')
  1947. teleportContainer.id = 'teleport'
  1948. document.body.appendChild(teleportContainer)
  1949. mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
  1950. h(Teleport, { to: '#teleport' }, [h('span', 'value')]),
  1951. )
  1952. expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
  1953. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1954. })
  1955. test('comment mismatch (element)', () => {
  1956. const { container } = mountWithHydration(`<div><span></span></div>`, () =>
  1957. h('div', [createCommentVNode('hi')]),
  1958. )
  1959. expect(container.innerHTML).toBe('<div><!--hi--></div>')
  1960. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1961. })
  1962. test('comment mismatch (text)', () => {
  1963. const { container } = mountWithHydration(`<div>foobar</div>`, () =>
  1964. h('div', [createCommentVNode('hi')]),
  1965. )
  1966. expect(container.innerHTML).toBe('<div><!--hi--></div>')
  1967. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1968. })
  1969. test('class mismatch', () => {
  1970. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1971. h('div', { class: ['foo', 'bar'] }),
  1972. )
  1973. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1974. h('div', { class: { foo: true, bar: true } }),
  1975. )
  1976. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1977. h('div', { class: 'foo bar' }),
  1978. )
  1979. // SVG classes
  1980. mountWithHydration(`<svg class="foo bar"></svg>`, () =>
  1981. h('svg', { class: 'foo bar' }),
  1982. )
  1983. // class with different order
  1984. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1985. h('div', { class: 'bar foo' }),
  1986. )
  1987. expect(`Hydration class mismatch`).not.toHaveBeenWarned()
  1988. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1989. h('div', { class: 'foo' }),
  1990. )
  1991. expect(`Hydration class mismatch`).toHaveBeenWarned()
  1992. })
  1993. test('style mismatch', () => {
  1994. mountWithHydration(`<div style="color:red;"></div>`, () =>
  1995. h('div', { style: { color: 'red' } }),
  1996. )
  1997. mountWithHydration(`<div style="color:red;"></div>`, () =>
  1998. h('div', { style: `color:red;` }),
  1999. )
  2000. mountWithHydration(
  2001. `<div style="color:red; font-size: 12px;"></div>`,
  2002. () => h('div', { style: `font-size: 12px; color:red;` }),
  2003. )
  2004. mountWithHydration(`<div style="color:red;display:none;"></div>`, () =>
  2005. withDirectives(createVNode('div', { style: 'color: red' }, ''), [
  2006. [vShow, false],
  2007. ]),
  2008. )
  2009. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  2010. mountWithHydration(`<div style="color:red;"></div>`, () =>
  2011. h('div', { style: { color: 'green' } }),
  2012. )
  2013. expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
  2014. })
  2015. test('style mismatch when no style attribute is present', () => {
  2016. mountWithHydration(`<div></div>`, () =>
  2017. h('div', { style: { color: 'red' } }),
  2018. )
  2019. expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
  2020. })
  2021. test('style mismatch w/ v-show', () => {
  2022. mountWithHydration(`<div style="color:red;display:none"></div>`, () =>
  2023. withDirectives(createVNode('div', { style: 'color: red' }, ''), [
  2024. [vShow, false],
  2025. ]),
  2026. )
  2027. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  2028. mountWithHydration(`<div style="color:red;"></div>`, () =>
  2029. withDirectives(createVNode('div', { style: 'color: red' }, ''), [
  2030. [vShow, false],
  2031. ]),
  2032. )
  2033. expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
  2034. })
  2035. test('attr mismatch', () => {
  2036. mountWithHydration(`<div id="foo"></div>`, () => h('div', { id: 'foo' }))
  2037. mountWithHydration(`<div spellcheck></div>`, () =>
  2038. h('div', { spellcheck: '' }),
  2039. )
  2040. mountWithHydration(`<div></div>`, () => h('div', { id: undefined }))
  2041. // boolean
  2042. mountWithHydration(`<select multiple></div>`, () =>
  2043. h('select', { multiple: true }),
  2044. )
  2045. mountWithHydration(`<select multiple></div>`, () =>
  2046. h('select', { multiple: 'multiple' }),
  2047. )
  2048. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  2049. mountWithHydration(`<div></div>`, () => h('div', { id: 'foo' }))
  2050. expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(1)
  2051. mountWithHydration(`<div id="bar"></div>`, () => h('div', { id: 'foo' }))
  2052. expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2)
  2053. })
  2054. test('attr special case: textarea value', () => {
  2055. mountWithHydration(`<textarea>foo</textarea>`, () =>
  2056. h('textarea', { value: 'foo' }),
  2057. )
  2058. mountWithHydration(`<textarea></textarea>`, () =>
  2059. h('textarea', { value: '' }),
  2060. )
  2061. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  2062. mountWithHydration(`<textarea>foo</textarea>`, () =>
  2063. h('textarea', { value: 'bar' }),
  2064. )
  2065. expect(`Hydration attribute mismatch`).toHaveBeenWarned()
  2066. })
  2067. // #11873
  2068. test('<textarea> with newlines at the beginning', async () => {
  2069. const render = () => h('textarea', null, '\nhello')
  2070. const html = await renderToString(createSSRApp({ render }))
  2071. mountWithHydration(html, render)
  2072. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  2073. })
  2074. test('<pre> with newlines at the beginning', async () => {
  2075. const render = () => h('pre', null, '\n')
  2076. const html = await renderToString(createSSRApp({ render }))
  2077. mountWithHydration(html, render)
  2078. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  2079. })
  2080. test('boolean attr handling', () => {
  2081. mountWithHydration(`<input />`, () => h('input', { readonly: false }))
  2082. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  2083. mountWithHydration(`<input readonly />`, () =>
  2084. h('input', { readonly: true }),
  2085. )
  2086. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  2087. mountWithHydration(`<input readonly="readonly" />`, () =>
  2088. h('input', { readonly: true }),
  2089. )
  2090. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  2091. })
  2092. test('client value is null or undefined', () => {
  2093. mountWithHydration(`<div></div>`, () =>
  2094. h('div', { draggable: undefined }),
  2095. )
  2096. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  2097. mountWithHydration(`<input />`, () => h('input', { type: null }))
  2098. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  2099. })
  2100. test('should not warn against object values', () => {
  2101. mountWithHydration(`<input />`, () => h('input', { from: {} }))
  2102. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  2103. })
  2104. test('should not warn on falsy bindings of non-property keys', () => {
  2105. mountWithHydration(`<button />`, () => h('button', { href: undefined }))
  2106. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  2107. })
  2108. test('should not warn on non-renderable option values', () => {
  2109. mountWithHydration(`<select><option>hello</option></select>`, () =>
  2110. h('select', [h('option', { value: ['foo'] }, 'hello')]),
  2111. )
  2112. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  2113. })
  2114. test('should not warn css v-bind', () => {
  2115. const container = document.createElement('div')
  2116. container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
  2117. const app = createSSRApp({
  2118. setup() {
  2119. useCssVars(() => ({
  2120. foo: 'red',
  2121. }))
  2122. return () => h('div', { style: { color: 'var(--foo)' } })
  2123. },
  2124. })
  2125. app.mount(container)
  2126. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  2127. })
  2128. // #10317 - test case from #10325
  2129. test('css vars should only be added to expected on component root dom', () => {
  2130. const container = document.createElement('div')
  2131. container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
  2132. const app = createSSRApp({
  2133. setup() {
  2134. useCssVars(() => ({
  2135. foo: 'red',
  2136. }))
  2137. return () =>
  2138. h('div', null, [h('div', { style: { color: 'var(--foo)' } })])
  2139. },
  2140. })
  2141. app.mount(container)
  2142. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  2143. })
  2144. // #11188
  2145. test('css vars support fallthrough', () => {
  2146. const container = document.createElement('div')
  2147. container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
  2148. const app = createSSRApp({
  2149. setup() {
  2150. useCssVars(() => ({
  2151. foo: 'red',
  2152. }))
  2153. return () => h(Child)
  2154. },
  2155. })
  2156. const Child = {
  2157. setup() {
  2158. return () => h('div', { style: 'padding: 4px' })
  2159. },
  2160. }
  2161. app.mount(container)
  2162. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  2163. })
  2164. // #11189
  2165. test('should not warn for directives that mutate DOM in created', () => {
  2166. const container = document.createElement('div')
  2167. container.innerHTML = `<div class="test red"></div>`
  2168. const vColor: ObjectDirective = {
  2169. created(el, binding) {
  2170. el.classList.add(binding.value)
  2171. },
  2172. }
  2173. const app = createSSRApp({
  2174. setup() {
  2175. return () =>
  2176. withDirectives(h('div', { class: 'test' }), [[vColor, 'red']])
  2177. },
  2178. })
  2179. app.mount(container)
  2180. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  2181. })
  2182. test('with disabled teleport + undefined target', async () => {
  2183. const container = document.createElement('div')
  2184. const isOpen = ref(false)
  2185. const App = {
  2186. setup() {
  2187. return { isOpen }
  2188. },
  2189. template: `
  2190. <Teleport :to="undefined" :disabled="true">
  2191. <div v-if="isOpen">
  2192. Menu is open...
  2193. </div>
  2194. </Teleport>`,
  2195. }
  2196. container.innerHTML = await renderToString(h(App))
  2197. const app = createSSRApp(App)
  2198. app.mount(container)
  2199. isOpen.value = true
  2200. await nextTick()
  2201. expect(container.innerHTML).toBe(
  2202. `<!--teleport start--><div> Menu is open... </div><!--teleport end-->`,
  2203. )
  2204. })
  2205. test('escape css var name', () => {
  2206. const container = document.createElement('div')
  2207. container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`
  2208. const app = createSSRApp({
  2209. setup() {
  2210. useCssVars(() => ({
  2211. 'foo.bar': 'red',
  2212. }))
  2213. return () => h(Child)
  2214. },
  2215. })
  2216. const Child = {
  2217. setup() {
  2218. return () => h('div', { style: 'padding: 4px' })
  2219. },
  2220. }
  2221. app.mount(container)
  2222. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  2223. })
  2224. })
  2225. describe('data-allow-mismatch', () => {
  2226. test('element text content', () => {
  2227. const { container } = mountWithHydration(
  2228. `<div data-allow-mismatch="text">foo</div>`,
  2229. () => h('div', 'bar'),
  2230. )
  2231. expect(container.innerHTML).toBe(
  2232. '<div data-allow-mismatch="text">bar</div>',
  2233. )
  2234. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  2235. })
  2236. test('not enough children', () => {
  2237. const { container } = mountWithHydration(
  2238. `<div data-allow-mismatch="children"></div>`,
  2239. () => h('div', [h('span', 'foo'), h('span', 'bar')]),
  2240. )
  2241. expect(container.innerHTML).toBe(
  2242. '<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>',
  2243. )
  2244. expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  2245. })
  2246. test('too many children', () => {
  2247. const { container } = mountWithHydration(
  2248. `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
  2249. () => h('div', [h('span', 'foo')]),
  2250. )
  2251. expect(container.innerHTML).toBe(
  2252. '<div data-allow-mismatch="children"><span>foo</span></div>',
  2253. )
  2254. expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  2255. })
  2256. test('complete mismatch', () => {
  2257. const { container } = mountWithHydration(
  2258. `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
  2259. () => h('div', [h('div', 'foo'), h('p', 'bar')]),
  2260. )
  2261. expect(container.innerHTML).toBe(
  2262. '<div data-allow-mismatch="children"><div>foo</div><p>bar</p></div>',
  2263. )
  2264. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  2265. })
  2266. test('fragment mismatch removal', () => {
  2267. const { container } = mountWithHydration(
  2268. `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  2269. () => h('div', [h('span', 'replaced')]),
  2270. )
  2271. expect(container.innerHTML).toBe(
  2272. '<div data-allow-mismatch="children"><span>replaced</span></div>',
  2273. )
  2274. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  2275. })
  2276. test('fragment not enough children', () => {
  2277. const { container } = mountWithHydration(
  2278. `<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  2279. () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
  2280. )
  2281. expect(container.innerHTML).toBe(
  2282. '<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
  2283. )
  2284. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  2285. })
  2286. test('fragment too many children', () => {
  2287. const { container } = mountWithHydration(
  2288. `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  2289. () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
  2290. )
  2291. expect(container.innerHTML).toBe(
  2292. '<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>',
  2293. )
  2294. // fragment ends early and attempts to hydrate the extra <div>bar</div>
  2295. // as 2nd fragment child.
  2296. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  2297. // excessive children removal
  2298. expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  2299. })
  2300. test('comment mismatch (element)', () => {
  2301. const { container } = mountWithHydration(
  2302. `<div data-allow-mismatch="children"><span></span></div>`,
  2303. () => h('div', [createCommentVNode('hi')]),
  2304. )
  2305. expect(container.innerHTML).toBe(
  2306. '<div data-allow-mismatch="children"><!--hi--></div>',
  2307. )
  2308. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  2309. })
  2310. test('comment mismatch (text)', () => {
  2311. const { container } = mountWithHydration(
  2312. `<div data-allow-mismatch="children">foobar</div>`,
  2313. () => h('div', [createCommentVNode('hi')]),
  2314. )
  2315. expect(container.innerHTML).toBe(
  2316. '<div data-allow-mismatch="children"><!--hi--></div>',
  2317. )
  2318. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  2319. })
  2320. test('class mismatch', () => {
  2321. mountWithHydration(
  2322. `<div class="foo bar" data-allow-mismatch="class"></div>`,
  2323. () => h('div', { class: 'foo' }),
  2324. )
  2325. expect(`Hydration class mismatch`).not.toHaveBeenWarned()
  2326. })
  2327. test('style mismatch', () => {
  2328. mountWithHydration(
  2329. `<div style="color:red;" data-allow-mismatch="style"></div>`,
  2330. () => h('div', { style: { color: 'green' } }),
  2331. )
  2332. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  2333. })
  2334. test('attr mismatch', () => {
  2335. mountWithHydration(`<div data-allow-mismatch="attribute"></div>`, () =>
  2336. h('div', { id: 'foo' }),
  2337. )
  2338. mountWithHydration(
  2339. `<div id="bar" data-allow-mismatch="attribute"></div>`,
  2340. () => h('div', { id: 'foo' }),
  2341. )
  2342. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  2343. })
  2344. })
  2345. })