hydration.spec.ts 58 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959
  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. openBlock,
  24. reactive,
  25. ref,
  26. renderSlot,
  27. useCssVars,
  28. vModelCheckbox,
  29. vShow,
  30. withCtx,
  31. withDirectives,
  32. } from '@vue/runtime-dom'
  33. import { type SSRContext, renderToString } from '@vue/server-renderer'
  34. import { PatchFlags, normalizeStyle } from '@vue/shared'
  35. import { vShowOriginalDisplay } from '../../runtime-dom/src/directives/vShow'
  36. import { expect } from 'vitest'
  37. function mountWithHydration(html: string, render: () => any) {
  38. const container = document.createElement('div')
  39. container.innerHTML = html
  40. const app = createSSRApp({
  41. render,
  42. })
  43. return {
  44. vnode: app.mount(container).$.subTree as VNode<Node, Element> & {
  45. el: Element
  46. },
  47. container,
  48. }
  49. }
  50. const triggerEvent = (type: string, el: Element) => {
  51. const event = new Event(type)
  52. el.dispatchEvent(event)
  53. }
  54. describe('SSR hydration', () => {
  55. beforeEach(() => {
  56. document.body.innerHTML = ''
  57. })
  58. test('text', async () => {
  59. const msg = ref('foo')
  60. const { vnode, container } = mountWithHydration('foo', () => msg.value)
  61. expect(vnode.el).toBe(container.firstChild)
  62. expect(container.textContent).toBe('foo')
  63. msg.value = 'bar'
  64. await nextTick()
  65. expect(container.textContent).toBe('bar')
  66. })
  67. test('empty text', async () => {
  68. const { container } = mountWithHydration('<div></div>', () =>
  69. h('div', createTextVNode('')),
  70. )
  71. expect(container.textContent).toBe('')
  72. expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
  73. })
  74. test('comment', () => {
  75. const { vnode, container } = mountWithHydration('<!---->', () => null)
  76. expect(vnode.el).toBe(container.firstChild)
  77. expect(vnode.el.nodeType).toBe(8) // comment
  78. })
  79. test('static', () => {
  80. const html = '<div><span>hello</span></div>'
  81. const { vnode, container } = mountWithHydration(html, () =>
  82. createStaticVNode('', 1),
  83. )
  84. expect(vnode.el).toBe(container.firstChild)
  85. expect(vnode.el.outerHTML).toBe(html)
  86. expect(vnode.anchor).toBe(container.firstChild)
  87. expect(vnode.children).toBe(html)
  88. })
  89. test('static (multiple elements)', () => {
  90. const staticContent = '<div></div><span>hello</span>'
  91. const html = `<div><div>hi</div>` + staticContent + `<div>ho</div></div>`
  92. const n1 = h('div', 'hi')
  93. const s = createStaticVNode('', 2)
  94. const n2 = h('div', 'ho')
  95. const { container } = mountWithHydration(html, () => h('div', [n1, s, n2]))
  96. const div = container.firstChild!
  97. expect(n1.el).toBe(div.firstChild)
  98. expect(n2.el).toBe(div.lastChild)
  99. expect(s.el).toBe(div.childNodes[1])
  100. expect(s.anchor).toBe(div.childNodes[2])
  101. expect(s.children).toBe(staticContent)
  102. })
  103. // #6008
  104. test('static (with text node as starting node)', () => {
  105. const html = ` A <span>foo</span> B`
  106. const { vnode, container } = mountWithHydration(html, () =>
  107. createStaticVNode(` A <span>foo</span> B`, 3),
  108. )
  109. expect(vnode.el).toBe(container.firstChild)
  110. expect(vnode.anchor).toBe(container.lastChild)
  111. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  112. })
  113. test('static with content adoption', () => {
  114. const html = ` A <span>foo</span> B`
  115. const { vnode, container } = mountWithHydration(html, () =>
  116. createStaticVNode(``, 3),
  117. )
  118. expect(vnode.el).toBe(container.firstChild)
  119. expect(vnode.anchor).toBe(container.lastChild)
  120. expect(vnode.children).toBe(html)
  121. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  122. })
  123. test('element with text children', async () => {
  124. const msg = ref('foo')
  125. const { vnode, container } = mountWithHydration(
  126. '<div class="foo">foo</div>',
  127. () => h('div', { class: msg.value }, msg.value),
  128. )
  129. expect(vnode.el).toBe(container.firstChild)
  130. expect(container.firstChild!.textContent).toBe('foo')
  131. msg.value = 'bar'
  132. await nextTick()
  133. expect(container.innerHTML).toBe(`<div class="bar">bar</div>`)
  134. })
  135. // #7285
  136. test('element with multiple continuous text vnodes', async () => {
  137. // should no mismatch warning
  138. const { container } = mountWithHydration('<div>fooo</div>', () =>
  139. h('div', ['fo', createTextVNode('o'), 'o']),
  140. )
  141. expect(container.textContent).toBe('fooo')
  142. })
  143. test('element with elements children', async () => {
  144. const msg = ref('foo')
  145. const fn = vi.fn()
  146. const { vnode, container } = mountWithHydration(
  147. '<div><span>foo</span><span class="foo"></span></div>',
  148. () =>
  149. h('div', [
  150. h('span', msg.value),
  151. h('span', { class: msg.value, onClick: fn }),
  152. ]),
  153. )
  154. expect(vnode.el).toBe(container.firstChild)
  155. expect((vnode.children as VNode[])[0].el).toBe(
  156. container.firstChild!.childNodes[0],
  157. )
  158. expect((vnode.children as VNode[])[1].el).toBe(
  159. container.firstChild!.childNodes[1],
  160. )
  161. // event handler
  162. triggerEvent('click', vnode.el.querySelector('.foo')!)
  163. expect(fn).toHaveBeenCalled()
  164. msg.value = 'bar'
  165. await nextTick()
  166. expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
  167. })
  168. test('element with ref', () => {
  169. const el = ref()
  170. const { vnode, container } = mountWithHydration('<div></div>', () =>
  171. h('div', { ref: el }),
  172. )
  173. expect(vnode.el).toBe(container.firstChild)
  174. expect(el.value).toBe(vnode.el)
  175. })
  176. test('Fragment', async () => {
  177. const msg = ref('foo')
  178. const fn = vi.fn()
  179. const { vnode, container } = mountWithHydration(
  180. '<div><!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]--></div>',
  181. () =>
  182. h('div', [
  183. [
  184. h('span', msg.value),
  185. [h('span', { class: msg.value, onClick: fn })],
  186. ],
  187. ]),
  188. )
  189. expect(vnode.el).toBe(container.firstChild)
  190. expect(vnode.el.innerHTML).toBe(
  191. `<!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]-->`,
  192. )
  193. // start fragment 1
  194. const fragment1 = (vnode.children as VNode[])[0]
  195. expect(fragment1.el).toBe(vnode.el.childNodes[0])
  196. const fragment1Children = fragment1.children as VNode[]
  197. // first <span>
  198. expect(fragment1Children[0].el!.tagName).toBe('SPAN')
  199. expect(fragment1Children[0].el).toBe(vnode.el.childNodes[1])
  200. // start fragment 2
  201. const fragment2 = fragment1Children[1]
  202. expect(fragment2.el).toBe(vnode.el.childNodes[2])
  203. const fragment2Children = fragment2.children as VNode[]
  204. // second <span>
  205. expect(fragment2Children[0].el!.tagName).toBe('SPAN')
  206. expect(fragment2Children[0].el).toBe(vnode.el.childNodes[3])
  207. // end fragment 2
  208. expect(fragment2.anchor).toBe(vnode.el.childNodes[4])
  209. // end fragment 1
  210. expect(fragment1.anchor).toBe(vnode.el.childNodes[5])
  211. // event handler
  212. triggerEvent('click', vnode.el.querySelector('.foo')!)
  213. expect(fn).toHaveBeenCalled()
  214. msg.value = 'bar'
  215. await nextTick()
  216. expect(vnode.el.innerHTML).toBe(
  217. `<!--[--><span>bar</span><!--[--><span class="bar"></span><!--]--><!--]-->`,
  218. )
  219. })
  220. // #7285
  221. test('Fragment (multiple continuous text vnodes)', async () => {
  222. // should no mismatch warning
  223. const { container } = mountWithHydration('<!--[-->fooo<!--]-->', () => [
  224. 'fo',
  225. createTextVNode('o'),
  226. 'o',
  227. ])
  228. expect(container.textContent).toBe('fooo')
  229. })
  230. test('Teleport', async () => {
  231. const msg = ref('foo')
  232. const fn = vi.fn()
  233. const teleportContainer = document.createElement('div')
  234. teleportContainer.id = 'teleport'
  235. teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!--teleport anchor-->`
  236. document.body.appendChild(teleportContainer)
  237. const { vnode, container } = mountWithHydration(
  238. '<!--teleport start--><!--teleport end-->',
  239. () =>
  240. h(Teleport, { to: '#teleport' }, [
  241. h('span', msg.value),
  242. h('span', { class: msg.value, onClick: fn }),
  243. ]),
  244. )
  245. expect(vnode.el).toBe(container.firstChild)
  246. expect(vnode.anchor).toBe(container.lastChild)
  247. expect(vnode.target).toBe(teleportContainer)
  248. expect((vnode.children as VNode[])[0].el).toBe(
  249. teleportContainer.childNodes[0],
  250. )
  251. expect((vnode.children as VNode[])[1].el).toBe(
  252. teleportContainer.childNodes[1],
  253. )
  254. expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2])
  255. // event handler
  256. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  257. expect(fn).toHaveBeenCalled()
  258. msg.value = 'bar'
  259. await nextTick()
  260. expect(teleportContainer.innerHTML).toBe(
  261. `<span>bar</span><span class="bar"></span><!--teleport anchor-->`,
  262. )
  263. })
  264. test('Teleport (multiple + integration)', async () => {
  265. const msg = ref('foo')
  266. const fn1 = vi.fn()
  267. const fn2 = vi.fn()
  268. const Comp = () => [
  269. h(Teleport, { to: '#teleport2' }, [
  270. h('span', msg.value),
  271. h('span', { class: msg.value, onClick: fn1 }),
  272. ]),
  273. h(Teleport, { to: '#teleport2' }, [
  274. h('span', msg.value + '2'),
  275. h('span', { class: msg.value + '2', onClick: fn2 }),
  276. ]),
  277. ]
  278. const teleportContainer = document.createElement('div')
  279. teleportContainer.id = 'teleport2'
  280. const ctx: SSRContext = {}
  281. const mainHtml = await renderToString(h(Comp), ctx)
  282. expect(mainHtml).toMatchInlineSnapshot(
  283. `"<!--[--><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--><!--]-->"`,
  284. )
  285. const teleportHtml = ctx.teleports!['#teleport2']
  286. expect(teleportHtml).toMatchInlineSnapshot(
  287. `"<span>foo</span><span class="foo"></span><!--teleport anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
  288. )
  289. teleportContainer.innerHTML = teleportHtml
  290. document.body.appendChild(teleportContainer)
  291. const { vnode, container } = mountWithHydration(mainHtml, Comp)
  292. expect(vnode.el).toBe(container.firstChild)
  293. const teleportVnode1 = (vnode.children as VNode[])[0]
  294. const teleportVnode2 = (vnode.children as VNode[])[1]
  295. expect(teleportVnode1.el).toBe(container.childNodes[1])
  296. expect(teleportVnode1.anchor).toBe(container.childNodes[2])
  297. expect(teleportVnode2.el).toBe(container.childNodes[3])
  298. expect(teleportVnode2.anchor).toBe(container.childNodes[4])
  299. expect(teleportVnode1.target).toBe(teleportContainer)
  300. expect((teleportVnode1 as any).children[0].el).toBe(
  301. teleportContainer.childNodes[0],
  302. )
  303. expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[2])
  304. expect(teleportVnode2.target).toBe(teleportContainer)
  305. expect((teleportVnode2 as any).children[0].el).toBe(
  306. teleportContainer.childNodes[3],
  307. )
  308. expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[5])
  309. // // event handler
  310. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  311. expect(fn1).toHaveBeenCalled()
  312. triggerEvent('click', teleportContainer.querySelector('.foo2')!)
  313. expect(fn2).toHaveBeenCalled()
  314. msg.value = 'bar'
  315. await nextTick()
  316. expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
  317. `"<span>bar</span><span class="bar"></span><!--teleport anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
  318. )
  319. })
  320. test('Teleport (disabled)', async () => {
  321. const msg = ref('foo')
  322. const fn1 = vi.fn()
  323. const fn2 = vi.fn()
  324. const Comp = () => [
  325. h('div', 'foo'),
  326. h(Teleport, { to: '#teleport3', disabled: true }, [
  327. h('span', msg.value),
  328. h('span', { class: msg.value, onClick: fn1 }),
  329. ]),
  330. h('div', { class: msg.value + '2', onClick: fn2 }, 'bar'),
  331. ]
  332. const teleportContainer = document.createElement('div')
  333. teleportContainer.id = 'teleport3'
  334. const ctx: SSRContext = {}
  335. const mainHtml = await renderToString(h(Comp), ctx)
  336. expect(mainHtml).toMatchInlineSnapshot(
  337. `"<!--[--><div>foo</div><!--teleport start--><span>foo</span><span class="foo"></span><!--teleport end--><div class="foo2">bar</div><!--]-->"`,
  338. )
  339. const teleportHtml = ctx.teleports!['#teleport3']
  340. expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)
  341. teleportContainer.innerHTML = teleportHtml
  342. document.body.appendChild(teleportContainer)
  343. const { vnode, container } = mountWithHydration(mainHtml, Comp)
  344. expect(vnode.el).toBe(container.firstChild)
  345. const children = vnode.children as VNode[]
  346. expect(children[0].el).toBe(container.childNodes[1])
  347. const teleportVnode = children[1]
  348. expect(teleportVnode.el).toBe(container.childNodes[2])
  349. expect((teleportVnode.children as VNode[])[0].el).toBe(
  350. container.childNodes[3],
  351. )
  352. expect((teleportVnode.children as VNode[])[1].el).toBe(
  353. container.childNodes[4],
  354. )
  355. expect(teleportVnode.anchor).toBe(container.childNodes[5])
  356. expect(children[2].el).toBe(container.childNodes[6])
  357. expect(teleportVnode.target).toBe(teleportContainer)
  358. expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[0])
  359. // // event handler
  360. triggerEvent('click', container.querySelector('.foo')!)
  361. expect(fn1).toHaveBeenCalled()
  362. triggerEvent('click', container.querySelector('.foo2')!)
  363. expect(fn2).toHaveBeenCalled()
  364. msg.value = 'bar'
  365. await nextTick()
  366. expect(container.innerHTML).toMatchInlineSnapshot(
  367. `"<!--[--><div>foo</div><!--teleport start--><span>bar</span><span class="bar"></span><!--teleport end--><div class="bar2">bar</div><!--]-->"`,
  368. )
  369. })
  370. // #6152
  371. test('Teleport (disabled + as component root)', () => {
  372. const { container } = mountWithHydration(
  373. '<!--[--><div>Parent fragment</div><!--teleport start--><div>Teleport content</div><!--teleport end--><!--]-->',
  374. () => [
  375. h('div', 'Parent fragment'),
  376. h(() =>
  377. h(Teleport, { to: 'body', disabled: true }, [
  378. h('div', 'Teleport content'),
  379. ]),
  380. ),
  381. ],
  382. )
  383. expect(document.body.innerHTML).toBe('')
  384. expect(container.innerHTML).toBe(
  385. '<!--[--><div>Parent fragment</div><!--teleport start--><div>Teleport content</div><!--teleport end--><!--]-->',
  386. )
  387. expect(
  388. `Hydration completed but contains mismatches.`,
  389. ).not.toHaveBeenWarned()
  390. })
  391. test('Teleport (as component root)', () => {
  392. const teleportContainer = document.createElement('div')
  393. teleportContainer.id = 'teleport4'
  394. teleportContainer.innerHTML = `hello<!--teleport anchor-->`
  395. document.body.appendChild(teleportContainer)
  396. const wrapper = {
  397. render() {
  398. return h(Teleport, { to: '#teleport4' }, ['hello'])
  399. },
  400. }
  401. const { vnode, container } = mountWithHydration(
  402. '<div><!--teleport start--><!--teleport end--><div></div></div>',
  403. () => h('div', [h(wrapper), h('div')]),
  404. )
  405. expect(vnode.el).toBe(container.firstChild)
  406. // component el
  407. const wrapperVNode = (vnode as any).children[0]
  408. const tpStart = container.firstChild?.firstChild
  409. const tpEnd = tpStart?.nextSibling
  410. expect(wrapperVNode.el).toBe(tpStart)
  411. expect(wrapperVNode.component.subTree.el).toBe(tpStart)
  412. expect(wrapperVNode.component.subTree.anchor).toBe(tpEnd)
  413. // next node hydrate properly
  414. const nextVNode = (vnode as any).children[1]
  415. expect(nextVNode.el).toBe(container.firstChild?.lastChild)
  416. })
  417. test('Teleport (nested)', () => {
  418. const teleportContainer = document.createElement('div')
  419. teleportContainer.id = 'teleport5'
  420. teleportContainer.innerHTML = `<div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><div>child</div><!--teleport anchor-->`
  421. document.body.appendChild(teleportContainer)
  422. const { vnode, container } = mountWithHydration(
  423. '<!--teleport start--><!--teleport end-->',
  424. () =>
  425. h(Teleport, { to: '#teleport5' }, [
  426. h('div', [h(Teleport, { to: '#teleport5' }, [h('div', 'child')])]),
  427. ]),
  428. )
  429. expect(vnode.el).toBe(container.firstChild)
  430. expect(vnode.anchor).toBe(container.lastChild)
  431. const childDivVNode = (vnode as any).children[0]
  432. const div = teleportContainer.firstChild
  433. expect(childDivVNode.el).toBe(div)
  434. expect(vnode.targetAnchor).toBe(div?.nextSibling)
  435. const childTeleportVNode = childDivVNode.children[0]
  436. expect(childTeleportVNode.el).toBe(div?.firstChild)
  437. expect(childTeleportVNode.anchor).toBe(div?.lastChild)
  438. expect(childTeleportVNode.targetAnchor).toBe(teleportContainer.lastChild)
  439. expect(childTeleportVNode.children[0].el).toBe(
  440. teleportContainer.lastChild?.previousSibling,
  441. )
  442. })
  443. // compile SSR + client render fn from the same template & hydrate
  444. test('full compiler integration', async () => {
  445. const mounted: string[] = []
  446. const log = vi.fn()
  447. const toggle = ref(true)
  448. const Child = {
  449. data() {
  450. return {
  451. count: 0,
  452. text: 'hello',
  453. style: {
  454. color: 'red',
  455. },
  456. }
  457. },
  458. mounted() {
  459. mounted.push('child')
  460. },
  461. template: `
  462. <div>
  463. <span class="count" :style="style">{{ count }}</span>
  464. <button class="inc" @click="count++">inc</button>
  465. <button class="change" @click="style.color = 'green'" >change color</button>
  466. <button class="emit" @click="$emit('foo')">emit</button>
  467. <span class="text">{{ text }}</span>
  468. <input v-model="text">
  469. </div>
  470. `,
  471. }
  472. const App = {
  473. setup() {
  474. return { toggle }
  475. },
  476. mounted() {
  477. mounted.push('parent')
  478. },
  479. template: `
  480. <div>
  481. <span>hello</span>
  482. <template v-if="toggle">
  483. <Child @foo="log('child')"/>
  484. <template v-if="true">
  485. <button class="parent-click" @click="log('click')">click me</button>
  486. </template>
  487. </template>
  488. <span>hello</span>
  489. </div>`,
  490. components: {
  491. Child,
  492. },
  493. methods: {
  494. log,
  495. },
  496. }
  497. const container = document.createElement('div')
  498. // server render
  499. container.innerHTML = await renderToString(h(App))
  500. // hydrate
  501. createSSRApp(App).mount(container)
  502. // assert interactions
  503. // 1. parent button click
  504. triggerEvent('click', container.querySelector('.parent-click')!)
  505. expect(log).toHaveBeenCalledWith('click')
  506. // 2. child inc click + text interpolation
  507. const count = container.querySelector('.count') as HTMLElement
  508. expect(count.textContent).toBe(`0`)
  509. triggerEvent('click', container.querySelector('.inc')!)
  510. await nextTick()
  511. expect(count.textContent).toBe(`1`)
  512. // 3. child color click + style binding
  513. expect(count.style.color).toBe('red')
  514. triggerEvent('click', container.querySelector('.change')!)
  515. await nextTick()
  516. expect(count.style.color).toBe('green')
  517. // 4. child event emit
  518. triggerEvent('click', container.querySelector('.emit')!)
  519. expect(log).toHaveBeenCalledWith('child')
  520. // 5. child v-model
  521. const text = container.querySelector('.text')!
  522. const input = container.querySelector('input')!
  523. expect(text.textContent).toBe('hello')
  524. input.value = 'bye'
  525. triggerEvent('input', input)
  526. await nextTick()
  527. expect(text.textContent).toBe('bye')
  528. })
  529. test('handle click error in ssr mode', async () => {
  530. const App = {
  531. setup() {
  532. const throwError = () => {
  533. throw new Error('Sentry Error')
  534. }
  535. return { throwError }
  536. },
  537. template: `
  538. <div>
  539. <button class="parent-click" @click="throwError">click me</button>
  540. </div>`,
  541. }
  542. const container = document.createElement('div')
  543. // server render
  544. container.innerHTML = await renderToString(h(App))
  545. // hydrate
  546. const app = createSSRApp(App)
  547. const handler = (app.config.errorHandler = vi.fn())
  548. app.mount(container)
  549. // assert interactions
  550. // parent button click
  551. triggerEvent('click', container.querySelector('.parent-click')!)
  552. expect(handler).toHaveBeenCalled()
  553. })
  554. test('handle blur error in ssr mode', async () => {
  555. const App = {
  556. setup() {
  557. const throwError = () => {
  558. throw new Error('Sentry Error')
  559. }
  560. return { throwError }
  561. },
  562. template: `
  563. <div>
  564. <input class="parent-click" @blur="throwError"/>
  565. </div>`,
  566. }
  567. const container = document.createElement('div')
  568. // server render
  569. container.innerHTML = await renderToString(h(App))
  570. // hydrate
  571. const app = createSSRApp(App)
  572. const handler = (app.config.errorHandler = vi.fn())
  573. app.mount(container)
  574. // assert interactions
  575. // parent blur event
  576. triggerEvent('blur', container.querySelector('.parent-click')!)
  577. expect(handler).toHaveBeenCalled()
  578. })
  579. test('Suspense', async () => {
  580. const AsyncChild = {
  581. async setup() {
  582. const count = ref(0)
  583. return () =>
  584. h(
  585. 'span',
  586. {
  587. onClick: () => {
  588. count.value++
  589. },
  590. },
  591. count.value,
  592. )
  593. },
  594. }
  595. const { vnode, container } = mountWithHydration('<span>0</span>', () =>
  596. h(Suspense, () => h(AsyncChild)),
  597. )
  598. expect(vnode.el).toBe(container.firstChild)
  599. // wait for hydration to finish
  600. await new Promise(r => setTimeout(r))
  601. triggerEvent('click', container.querySelector('span')!)
  602. await nextTick()
  603. expect(container.innerHTML).toBe(`<span>1</span>`)
  604. })
  605. // #6638
  606. test('Suspense + async component', async () => {
  607. let isSuspenseResolved = false
  608. let isSuspenseResolvedInChild: any
  609. const AsyncChild = defineAsyncComponent(() =>
  610. Promise.resolve(
  611. defineComponent({
  612. setup() {
  613. isSuspenseResolvedInChild = isSuspenseResolved
  614. const count = ref(0)
  615. return () =>
  616. h(
  617. 'span',
  618. {
  619. onClick: () => {
  620. count.value++
  621. },
  622. },
  623. count.value,
  624. )
  625. },
  626. }),
  627. ),
  628. )
  629. const { vnode, container } = mountWithHydration('<span>0</span>', () =>
  630. h(
  631. Suspense,
  632. {
  633. onResolve() {
  634. isSuspenseResolved = true
  635. },
  636. },
  637. () => h(AsyncChild),
  638. ),
  639. )
  640. expect(vnode.el).toBe(container.firstChild)
  641. // wait for hydration to finish
  642. await new Promise(r => setTimeout(r))
  643. expect(isSuspenseResolvedInChild).toBe(false)
  644. expect(isSuspenseResolved).toBe(true)
  645. // assert interaction
  646. triggerEvent('click', container.querySelector('span')!)
  647. await nextTick()
  648. expect(container.innerHTML).toBe(`<span>1</span>`)
  649. })
  650. test('Suspense (full integration)', async () => {
  651. const mountedCalls: number[] = []
  652. const asyncDeps: Promise<any>[] = []
  653. const AsyncChild = defineComponent({
  654. props: ['n'],
  655. async setup(props) {
  656. const count = ref(props.n)
  657. onMounted(() => {
  658. mountedCalls.push(props.n)
  659. })
  660. const p = new Promise(r => setTimeout(r, props.n * 10))
  661. asyncDeps.push(p)
  662. await p
  663. return () =>
  664. h(
  665. 'span',
  666. {
  667. onClick: () => {
  668. count.value++
  669. },
  670. },
  671. count.value,
  672. )
  673. },
  674. })
  675. const done = vi.fn()
  676. const App = {
  677. template: `
  678. <Suspense @resolve="done">
  679. <div>
  680. <AsyncChild :n="1" />
  681. <AsyncChild :n="2" />
  682. </div>
  683. </Suspense>`,
  684. components: {
  685. AsyncChild,
  686. },
  687. methods: {
  688. done,
  689. },
  690. }
  691. const container = document.createElement('div')
  692. // server render
  693. container.innerHTML = await renderToString(h(App))
  694. expect(container.innerHTML).toMatchInlineSnapshot(
  695. `"<div><span>1</span><span>2</span></div>"`,
  696. )
  697. // reset asyncDeps from ssr
  698. asyncDeps.length = 0
  699. // hydrate
  700. createSSRApp(App).mount(container)
  701. expect(mountedCalls.length).toBe(0)
  702. expect(asyncDeps.length).toBe(2)
  703. // wait for hydration to complete
  704. await Promise.all(asyncDeps)
  705. await new Promise(r => setTimeout(r))
  706. // should flush buffered effects
  707. expect(mountedCalls).toMatchObject([1, 2])
  708. expect(container.innerHTML).toMatch(
  709. `<div><span>1</span><span>2</span></div>`,
  710. )
  711. const span1 = container.querySelector('span')!
  712. triggerEvent('click', span1)
  713. await nextTick()
  714. expect(container.innerHTML).toMatch(
  715. `<div><span>2</span><span>2</span></div>`,
  716. )
  717. const span2 = span1.nextSibling as Element
  718. triggerEvent('click', span2)
  719. await nextTick()
  720. expect(container.innerHTML).toMatch(
  721. `<div><span>2</span><span>3</span></div>`,
  722. )
  723. })
  724. test('async component', async () => {
  725. const spy = vi.fn()
  726. const Comp = () =>
  727. h(
  728. 'button',
  729. {
  730. onClick: spy,
  731. },
  732. 'hello!',
  733. )
  734. let serverResolve: any
  735. let AsyncComp = defineAsyncComponent(
  736. () =>
  737. new Promise(r => {
  738. serverResolve = r
  739. }),
  740. )
  741. const App = {
  742. render() {
  743. return ['hello', h(AsyncComp), 'world']
  744. },
  745. }
  746. // server render
  747. const htmlPromise = renderToString(h(App))
  748. serverResolve(Comp)
  749. const html = await htmlPromise
  750. expect(html).toMatchInlineSnapshot(
  751. `"<!--[-->hello<button>hello!</button>world<!--]-->"`,
  752. )
  753. // hydration
  754. let clientResolve: any
  755. AsyncComp = defineAsyncComponent(
  756. () =>
  757. new Promise(r => {
  758. clientResolve = r
  759. }),
  760. )
  761. const container = document.createElement('div')
  762. container.innerHTML = html
  763. createSSRApp(App).mount(container)
  764. // hydration not complete yet
  765. triggerEvent('click', container.querySelector('button')!)
  766. expect(spy).not.toHaveBeenCalled()
  767. // resolve
  768. clientResolve(Comp)
  769. await new Promise(r => setTimeout(r))
  770. // should be hydrated now
  771. triggerEvent('click', container.querySelector('button')!)
  772. expect(spy).toHaveBeenCalled()
  773. })
  774. test('update async wrapper before resolve', async () => {
  775. const Comp = {
  776. render() {
  777. return h('h1', 'Async component')
  778. },
  779. }
  780. let serverResolve: any
  781. let AsyncComp = defineAsyncComponent(
  782. () =>
  783. new Promise(r => {
  784. serverResolve = r
  785. }),
  786. )
  787. const toggle = ref(true)
  788. const App = {
  789. setup() {
  790. onMounted(() => {
  791. // change state, this makes updateComponent(AsyncComp) execute before
  792. // the async component is resolved
  793. toggle.value = false
  794. })
  795. return () => {
  796. return [toggle.value ? 'hello' : 'world', h(AsyncComp)]
  797. }
  798. },
  799. }
  800. // server render
  801. const htmlPromise = renderToString(h(App))
  802. serverResolve(Comp)
  803. const html = await htmlPromise
  804. expect(html).toMatchInlineSnapshot(
  805. `"<!--[-->hello<h1>Async component</h1><!--]-->"`,
  806. )
  807. // hydration
  808. let clientResolve: any
  809. AsyncComp = defineAsyncComponent(
  810. () =>
  811. new Promise(r => {
  812. clientResolve = r
  813. }),
  814. )
  815. const container = document.createElement('div')
  816. container.innerHTML = html
  817. createSSRApp(App).mount(container)
  818. // resolve
  819. clientResolve(Comp)
  820. await new Promise(r => setTimeout(r))
  821. // should be hydrated now
  822. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  823. expect(container.innerHTML).toMatchInlineSnapshot(
  824. `"<!--[-->world<h1>Async component</h1><!--]-->"`,
  825. )
  826. })
  827. test('hydrate safely when property used by async setup changed before render', async () => {
  828. const toggle = ref(true)
  829. const AsyncComp = {
  830. async setup() {
  831. await new Promise<void>(r => setTimeout(r, 10))
  832. return () => h('h1', 'Async component')
  833. },
  834. }
  835. const AsyncWrapper = {
  836. render() {
  837. return h(AsyncComp)
  838. },
  839. }
  840. const SiblingComp = {
  841. setup() {
  842. toggle.value = false
  843. return () => h('span')
  844. },
  845. }
  846. const App = {
  847. setup() {
  848. return () =>
  849. h(
  850. Suspense,
  851. {},
  852. {
  853. default: () => [
  854. h('main', {}, [
  855. h(AsyncWrapper, {
  856. prop: toggle.value ? 'hello' : 'world',
  857. }),
  858. h(SiblingComp),
  859. ]),
  860. ],
  861. },
  862. )
  863. },
  864. }
  865. // server render
  866. const html = await renderToString(h(App))
  867. expect(html).toMatchInlineSnapshot(
  868. `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
  869. )
  870. expect(toggle.value).toBe(false)
  871. // hydration
  872. // reset the value
  873. toggle.value = true
  874. expect(toggle.value).toBe(true)
  875. const container = document.createElement('div')
  876. container.innerHTML = html
  877. createSSRApp(App).mount(container)
  878. await new Promise(r => setTimeout(r, 10))
  879. expect(toggle.value).toBe(false)
  880. // should be hydrated now
  881. expect(container.innerHTML).toMatchInlineSnapshot(
  882. `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
  883. )
  884. })
  885. test('hydrate safely when property used by deep nested async setup changed before render', async () => {
  886. const toggle = ref(true)
  887. const AsyncComp = {
  888. async setup() {
  889. await new Promise<void>(r => setTimeout(r, 10))
  890. return () => h('h1', 'Async component')
  891. },
  892. }
  893. const AsyncWrapper = { render: () => h(AsyncComp) }
  894. const AsyncWrapperWrapper = { render: () => h(AsyncWrapper) }
  895. const SiblingComp = {
  896. setup() {
  897. toggle.value = false
  898. return () => h('span')
  899. },
  900. }
  901. const App = {
  902. setup() {
  903. return () =>
  904. h(
  905. Suspense,
  906. {},
  907. {
  908. default: () => [
  909. h('main', {}, [
  910. h(AsyncWrapperWrapper, {
  911. prop: toggle.value ? 'hello' : 'world',
  912. }),
  913. h(SiblingComp),
  914. ]),
  915. ],
  916. },
  917. )
  918. },
  919. }
  920. // server render
  921. const html = await renderToString(h(App))
  922. expect(html).toMatchInlineSnapshot(
  923. `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
  924. )
  925. expect(toggle.value).toBe(false)
  926. // hydration
  927. // reset the value
  928. toggle.value = true
  929. expect(toggle.value).toBe(true)
  930. const container = document.createElement('div')
  931. container.innerHTML = html
  932. createSSRApp(App).mount(container)
  933. await new Promise(r => setTimeout(r, 10))
  934. expect(toggle.value).toBe(false)
  935. // should be hydrated now
  936. expect(container.innerHTML).toMatchInlineSnapshot(
  937. `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
  938. )
  939. })
  940. // #3787
  941. test('unmount async wrapper before load', async () => {
  942. let resolve: any
  943. const AsyncComp = defineAsyncComponent(
  944. () =>
  945. new Promise(r => {
  946. resolve = r
  947. }),
  948. )
  949. const show = ref(true)
  950. const root = document.createElement('div')
  951. root.innerHTML = '<div><div>async</div></div>'
  952. createSSRApp({
  953. render() {
  954. return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
  955. },
  956. }).mount(root)
  957. show.value = false
  958. await nextTick()
  959. expect(root.innerHTML).toBe('<div><div>hi</div></div>')
  960. resolve({})
  961. })
  962. test('unmount async wrapper before load (fragment)', async () => {
  963. let resolve: any
  964. const AsyncComp = defineAsyncComponent(
  965. () =>
  966. new Promise(r => {
  967. resolve = r
  968. }),
  969. )
  970. const show = ref(true)
  971. const root = document.createElement('div')
  972. root.innerHTML = '<div><!--[-->async<!--]--></div>'
  973. createSSRApp({
  974. render() {
  975. return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
  976. },
  977. }).mount(root)
  978. show.value = false
  979. await nextTick()
  980. expect(root.innerHTML).toBe('<div><div>hi</div></div>')
  981. resolve({})
  982. })
  983. test('elements with camel-case in svg ', () => {
  984. const { vnode, container } = mountWithHydration(
  985. '<animateTransform></animateTransform>',
  986. () => h('animateTransform'),
  987. )
  988. expect(vnode.el).toBe(container.firstChild)
  989. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  990. })
  991. test('SVG as a mount container', () => {
  992. const svgContainer = document.createElement('svg')
  993. svgContainer.innerHTML = '<g></g>'
  994. const app = createSSRApp({
  995. render: () => h('g'),
  996. })
  997. expect(
  998. (
  999. app.mount(svgContainer).$.subTree as VNode<Node, Element> & {
  1000. el: Element
  1001. }
  1002. ).el instanceof SVGElement,
  1003. )
  1004. })
  1005. test('force hydrate prop with `.prop` modifier', () => {
  1006. const { container } = mountWithHydration('<input type="checkbox">', () =>
  1007. h('input', {
  1008. type: 'checkbox',
  1009. '.indeterminate': true,
  1010. }),
  1011. )
  1012. expect((container.firstChild! as any).indeterminate).toBe(true)
  1013. })
  1014. test('force hydrate input v-model with non-string value bindings', () => {
  1015. const { container } = mountWithHydration(
  1016. '<input type="checkbox" value="true">',
  1017. () =>
  1018. withDirectives(
  1019. createVNode(
  1020. 'input',
  1021. { type: 'checkbox', 'true-value': true },
  1022. null,
  1023. PatchFlags.PROPS,
  1024. ['true-value'],
  1025. ),
  1026. [[vModelCheckbox, true]],
  1027. ),
  1028. )
  1029. expect((container.firstChild as any)._trueValue).toBe(true)
  1030. })
  1031. test('force hydrate checkbox with indeterminate', () => {
  1032. const { container } = mountWithHydration(
  1033. '<input type="checkbox" indeterminate>',
  1034. () =>
  1035. createVNode(
  1036. 'input',
  1037. { type: 'checkbox', indeterminate: '' },
  1038. null,
  1039. PatchFlags.CACHED,
  1040. ),
  1041. )
  1042. expect((container.firstChild as any).indeterminate).toBe(true)
  1043. })
  1044. test('force hydrate select option with non-string value bindings', () => {
  1045. const { container } = mountWithHydration(
  1046. '<select><option value="true">ok</option></select>',
  1047. () =>
  1048. h('select', [
  1049. // hoisted because bound value is a constant...
  1050. createVNode('option', { value: true }, null, -1 /* HOISTED */),
  1051. ]),
  1052. )
  1053. expect((container.firstChild!.firstChild as any)._value).toBe(true)
  1054. })
  1055. // #5728
  1056. test('empty text node in slot', () => {
  1057. const Comp = {
  1058. render(this: any) {
  1059. return renderSlot(this.$slots, 'default', {}, () => [
  1060. createTextVNode(''),
  1061. ])
  1062. },
  1063. }
  1064. const { container, vnode } = mountWithHydration('<!--[--><!--]-->', () =>
  1065. h(Comp),
  1066. )
  1067. expect(container.childNodes.length).toBe(3)
  1068. const text = container.childNodes[1]
  1069. expect(text.nodeType).toBe(3)
  1070. expect(vnode.el).toBe(container.childNodes[0])
  1071. // component => slot fragment => text node
  1072. expect((vnode as any).component?.subTree.children[0].el).toBe(text)
  1073. })
  1074. // #7215
  1075. test('empty text node', () => {
  1076. const Comp = {
  1077. render(this: any) {
  1078. return h('p', [''])
  1079. },
  1080. }
  1081. const { container } = mountWithHydration('<p></p>', () => h(Comp))
  1082. expect(container.childNodes.length).toBe(1)
  1083. const p = container.childNodes[0]
  1084. expect(p.childNodes.length).toBe(1)
  1085. const text = p.childNodes[0]
  1086. expect(text.nodeType).toBe(3)
  1087. })
  1088. // #11372
  1089. test('object style value tracking in prod', async () => {
  1090. __DEV__ = false
  1091. try {
  1092. const style = reactive({ color: 'red' })
  1093. const Comp = {
  1094. render(this: any) {
  1095. return (
  1096. openBlock(),
  1097. createElementBlock(
  1098. 'div',
  1099. {
  1100. style: normalizeStyle(style),
  1101. },
  1102. null,
  1103. 4 /* STYLE */,
  1104. )
  1105. )
  1106. },
  1107. }
  1108. const { container } = mountWithHydration(
  1109. `<div style="color: red;"></div>`,
  1110. () => h(Comp),
  1111. )
  1112. style.color = 'green'
  1113. await nextTick()
  1114. expect(container.innerHTML).toBe(`<div style="color: green;"></div>`)
  1115. } finally {
  1116. __DEV__ = true
  1117. }
  1118. })
  1119. test('app.unmount()', async () => {
  1120. const container = document.createElement('DIV')
  1121. container.innerHTML = '<button></button>'
  1122. const App = defineComponent({
  1123. setup(_, { expose }) {
  1124. const count = ref(0)
  1125. expose({ count })
  1126. return () =>
  1127. h('button', {
  1128. onClick: () => count.value++,
  1129. })
  1130. },
  1131. })
  1132. const app = createSSRApp(App)
  1133. const vm = app.mount(container)
  1134. await nextTick()
  1135. expect((container as any)._vnode).toBeDefined()
  1136. // @ts-expect-error - expose()'d properties are not available on vm type
  1137. expect(vm.count).toBe(0)
  1138. app.unmount()
  1139. expect((container as any)._vnode).toBe(null)
  1140. })
  1141. // #6637
  1142. test('stringified root fragment', () => {
  1143. mountWithHydration(`<!--[--><div></div><!--]-->`, () =>
  1144. createStaticVNode(`<div></div>`, 1),
  1145. )
  1146. expect(`mismatch`).not.toHaveBeenWarned()
  1147. })
  1148. test('transition appear', () => {
  1149. const { vnode, container } = mountWithHydration(
  1150. `<template><div>foo</div></template>`,
  1151. () =>
  1152. h(
  1153. Transition,
  1154. { appear: true },
  1155. {
  1156. default: () => h('div', 'foo'),
  1157. },
  1158. ),
  1159. )
  1160. expect(container.firstChild).toMatchInlineSnapshot(`
  1161. <div
  1162. class="v-enter-from v-enter-active"
  1163. >
  1164. foo
  1165. </div>
  1166. `)
  1167. expect(vnode.el).toBe(container.firstChild)
  1168. expect(`mismatch`).not.toHaveBeenWarned()
  1169. })
  1170. test('transition appear with v-if', () => {
  1171. const show = false
  1172. const { vnode, container } = mountWithHydration(
  1173. `<template><!----></template>`,
  1174. () =>
  1175. h(
  1176. Transition,
  1177. { appear: true },
  1178. {
  1179. default: () => (show ? h('div', 'foo') : createCommentVNode('')),
  1180. },
  1181. ),
  1182. )
  1183. expect(container.firstChild).toMatchInlineSnapshot('<!---->')
  1184. expect(vnode.el).toBe(container.firstChild)
  1185. expect(`mismatch`).not.toHaveBeenWarned()
  1186. })
  1187. test('transition appear with v-show', () => {
  1188. const show = false
  1189. const { vnode, container } = mountWithHydration(
  1190. `<template><div style="display: none;">foo</div></template>`,
  1191. () =>
  1192. h(
  1193. Transition,
  1194. { appear: true },
  1195. {
  1196. default: () =>
  1197. withDirectives(createVNode('div', null, 'foo'), [[vShow, show]]),
  1198. },
  1199. ),
  1200. )
  1201. expect(container.firstChild).toMatchInlineSnapshot(`
  1202. <div
  1203. class="v-enter-from v-enter-active"
  1204. style="display: none;"
  1205. >
  1206. foo
  1207. </div>
  1208. `)
  1209. expect((container.firstChild as any)[vShowOriginalDisplay]).toBe('')
  1210. expect(vnode.el).toBe(container.firstChild)
  1211. expect(`mismatch`).not.toHaveBeenWarned()
  1212. })
  1213. test('transition appear w/ event listener', async () => {
  1214. const container = document.createElement('div')
  1215. container.innerHTML = `<template><button>0</button></template>`
  1216. createSSRApp({
  1217. data() {
  1218. return {
  1219. count: 0,
  1220. }
  1221. },
  1222. template: `
  1223. <Transition appear>
  1224. <button @click="count++">{{count}}</button>
  1225. </Transition>
  1226. `,
  1227. }).mount(container)
  1228. expect(container.firstChild).toMatchInlineSnapshot(`
  1229. <button
  1230. class="v-enter-from v-enter-active"
  1231. >
  1232. 0
  1233. </button>
  1234. `)
  1235. triggerEvent('click', container.querySelector('button')!)
  1236. await nextTick()
  1237. expect(container.firstChild).toMatchInlineSnapshot(`
  1238. <button
  1239. class="v-enter-from v-enter-active"
  1240. >
  1241. 1
  1242. </button>
  1243. `)
  1244. })
  1245. // #10607
  1246. test('update component stable slot (prod + optimized mode)', async () => {
  1247. __DEV__ = false
  1248. try {
  1249. const container = document.createElement('div')
  1250. container.innerHTML = `<template><div show="false"><!--[--><div><div><!----></div></div><div>0</div><!--]--></div></template>`
  1251. const Comp = {
  1252. render(this: any) {
  1253. return (
  1254. openBlock(),
  1255. createElementBlock('div', null, [
  1256. renderSlot(this.$slots, 'default'),
  1257. ])
  1258. )
  1259. },
  1260. }
  1261. const show = ref(false)
  1262. const clicked = ref(false)
  1263. const Wrapper = {
  1264. setup() {
  1265. const items = ref<number[]>([])
  1266. onMounted(() => {
  1267. items.value = [1]
  1268. })
  1269. return () => {
  1270. return (
  1271. openBlock(),
  1272. createBlock(Comp, null, {
  1273. default: withCtx(() => [
  1274. createElementVNode('div', null, [
  1275. createElementVNode('div', null, [
  1276. clicked.value
  1277. ? (openBlock(),
  1278. createElementBlock('div', { key: 0 }, 'foo'))
  1279. : createCommentVNode('v-if', true),
  1280. ]),
  1281. ]),
  1282. createElementVNode(
  1283. 'div',
  1284. null,
  1285. items.value.length,
  1286. 1 /* TEXT */,
  1287. ),
  1288. ]),
  1289. _: 1 /* STABLE */,
  1290. })
  1291. )
  1292. }
  1293. },
  1294. }
  1295. createSSRApp({
  1296. components: { Wrapper },
  1297. data() {
  1298. return { show }
  1299. },
  1300. template: `<Wrapper :show="show"/>`,
  1301. }).mount(container)
  1302. await nextTick()
  1303. expect(container.innerHTML).toBe(
  1304. `<div show="false"><!--[--><div><div><!----></div></div><div>1</div><!--]--></div>`,
  1305. )
  1306. show.value = true
  1307. await nextTick()
  1308. expect(async () => {
  1309. clicked.value = true
  1310. await nextTick()
  1311. }).not.toThrow("Cannot read properties of null (reading 'insertBefore')")
  1312. await nextTick()
  1313. expect(container.innerHTML).toBe(
  1314. `<div show="true"><!--[--><div><div><div>foo</div></div></div><div>1</div><!--]--></div>`,
  1315. )
  1316. } catch (e) {
  1317. throw e
  1318. } finally {
  1319. __DEV__ = true
  1320. }
  1321. })
  1322. describe('mismatch handling', () => {
  1323. test('text node', () => {
  1324. const { container } = mountWithHydration(`foo`, () => 'bar')
  1325. expect(container.textContent).toBe('bar')
  1326. expect(`Hydration text mismatch`).toHaveBeenWarned()
  1327. })
  1328. test('element text content', () => {
  1329. const { container } = mountWithHydration(`<div>foo</div>`, () =>
  1330. h('div', 'bar'),
  1331. )
  1332. expect(container.innerHTML).toBe('<div>bar</div>')
  1333. expect(`Hydration text content mismatch`).toHaveBeenWarned()
  1334. })
  1335. test('not enough children', () => {
  1336. const { container } = mountWithHydration(`<div></div>`, () =>
  1337. h('div', [h('span', 'foo'), h('span', 'bar')]),
  1338. )
  1339. expect(container.innerHTML).toBe(
  1340. '<div><span>foo</span><span>bar</span></div>',
  1341. )
  1342. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1343. })
  1344. test('too many children', () => {
  1345. const { container } = mountWithHydration(
  1346. `<div><span>foo</span><span>bar</span></div>`,
  1347. () => h('div', [h('span', 'foo')]),
  1348. )
  1349. expect(container.innerHTML).toBe('<div><span>foo</span></div>')
  1350. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1351. })
  1352. test('complete mismatch', () => {
  1353. const { container } = mountWithHydration(
  1354. `<div><span>foo</span><span>bar</span></div>`,
  1355. () => h('div', [h('div', 'foo'), h('p', 'bar')]),
  1356. )
  1357. expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
  1358. expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
  1359. })
  1360. test('fragment mismatch removal', () => {
  1361. const { container } = mountWithHydration(
  1362. `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  1363. () => h('div', [h('span', 'replaced')]),
  1364. )
  1365. expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
  1366. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1367. })
  1368. test('fragment not enough children', () => {
  1369. const { container } = mountWithHydration(
  1370. `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  1371. () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
  1372. )
  1373. expect(container.innerHTML).toBe(
  1374. '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
  1375. )
  1376. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1377. })
  1378. test('fragment too many children', () => {
  1379. const { container } = mountWithHydration(
  1380. `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  1381. () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
  1382. )
  1383. expect(container.innerHTML).toBe(
  1384. '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>',
  1385. )
  1386. // fragment ends early and attempts to hydrate the extra <div>bar</div>
  1387. // as 2nd fragment child.
  1388. expect(`Hydration text content mismatch`).toHaveBeenWarned()
  1389. // excessive children removal
  1390. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1391. })
  1392. test('Teleport target has empty children', () => {
  1393. const teleportContainer = document.createElement('div')
  1394. teleportContainer.id = 'teleport'
  1395. document.body.appendChild(teleportContainer)
  1396. mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
  1397. h(Teleport, { to: '#teleport' }, [h('span', 'value')]),
  1398. )
  1399. expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
  1400. expect(`Hydration children mismatch`).toHaveBeenWarned()
  1401. })
  1402. test('comment mismatch (element)', () => {
  1403. const { container } = mountWithHydration(`<div><span></span></div>`, () =>
  1404. h('div', [createCommentVNode('hi')]),
  1405. )
  1406. expect(container.innerHTML).toBe('<div><!--hi--></div>')
  1407. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1408. })
  1409. test('comment mismatch (text)', () => {
  1410. const { container } = mountWithHydration(`<div>foobar</div>`, () =>
  1411. h('div', [createCommentVNode('hi')]),
  1412. )
  1413. expect(container.innerHTML).toBe('<div><!--hi--></div>')
  1414. expect(`Hydration node mismatch`).toHaveBeenWarned()
  1415. })
  1416. test('class mismatch', () => {
  1417. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1418. h('div', { class: ['foo', 'bar'] }),
  1419. )
  1420. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1421. h('div', { class: { foo: true, bar: true } }),
  1422. )
  1423. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1424. h('div', { class: 'foo bar' }),
  1425. )
  1426. // SVG classes
  1427. mountWithHydration(`<svg class="foo bar"></svg>`, () =>
  1428. h('svg', { class: 'foo bar' }),
  1429. )
  1430. // class with different order
  1431. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1432. h('div', { class: 'bar foo' }),
  1433. )
  1434. expect(`Hydration class mismatch`).not.toHaveBeenWarned()
  1435. mountWithHydration(`<div class="foo bar"></div>`, () =>
  1436. h('div', { class: 'foo' }),
  1437. )
  1438. expect(`Hydration class mismatch`).toHaveBeenWarned()
  1439. })
  1440. test('style mismatch', () => {
  1441. mountWithHydration(`<div style="color:red;"></div>`, () =>
  1442. h('div', { style: { color: 'red' } }),
  1443. )
  1444. mountWithHydration(`<div style="color:red;"></div>`, () =>
  1445. h('div', { style: `color:red;` }),
  1446. )
  1447. mountWithHydration(
  1448. `<div style="color:red; font-size: 12px;"></div>`,
  1449. () => h('div', { style: `font-size: 12px; color:red;` }),
  1450. )
  1451. mountWithHydration(`<div style="color:red;display:none;"></div>`, () =>
  1452. withDirectives(createVNode('div', { style: 'color: red' }, ''), [
  1453. [vShow, false],
  1454. ]),
  1455. )
  1456. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  1457. mountWithHydration(`<div style="color:red;"></div>`, () =>
  1458. h('div', { style: { color: 'green' } }),
  1459. )
  1460. expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
  1461. })
  1462. test('style mismatch when no style attribute is present', () => {
  1463. mountWithHydration(`<div></div>`, () =>
  1464. h('div', { style: { color: 'red' } }),
  1465. )
  1466. expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
  1467. })
  1468. test('style mismatch w/ v-show', () => {
  1469. mountWithHydration(`<div style="color:red;display:none"></div>`, () =>
  1470. withDirectives(createVNode('div', { style: 'color: red' }, ''), [
  1471. [vShow, false],
  1472. ]),
  1473. )
  1474. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  1475. mountWithHydration(`<div style="color:red;"></div>`, () =>
  1476. withDirectives(createVNode('div', { style: 'color: red' }, ''), [
  1477. [vShow, false],
  1478. ]),
  1479. )
  1480. expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
  1481. })
  1482. test('attr mismatch', () => {
  1483. mountWithHydration(`<div id="foo"></div>`, () => h('div', { id: 'foo' }))
  1484. mountWithHydration(`<div spellcheck></div>`, () =>
  1485. h('div', { spellcheck: '' }),
  1486. )
  1487. mountWithHydration(`<div></div>`, () => h('div', { id: undefined }))
  1488. // boolean
  1489. mountWithHydration(`<select multiple></div>`, () =>
  1490. h('select', { multiple: true }),
  1491. )
  1492. mountWithHydration(`<select multiple></div>`, () =>
  1493. h('select', { multiple: 'multiple' }),
  1494. )
  1495. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1496. mountWithHydration(`<div></div>`, () => h('div', { id: 'foo' }))
  1497. expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(1)
  1498. mountWithHydration(`<div id="bar"></div>`, () => h('div', { id: 'foo' }))
  1499. expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2)
  1500. })
  1501. test('attr special case: textarea value', () => {
  1502. mountWithHydration(`<textarea>foo</textarea>`, () =>
  1503. h('textarea', { value: 'foo' }),
  1504. )
  1505. mountWithHydration(`<textarea></textarea>`, () =>
  1506. h('textarea', { value: '' }),
  1507. )
  1508. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1509. mountWithHydration(`<textarea>foo</textarea>`, () =>
  1510. h('textarea', { value: 'bar' }),
  1511. )
  1512. expect(`Hydration attribute mismatch`).toHaveBeenWarned()
  1513. })
  1514. test('boolean attr handling', () => {
  1515. mountWithHydration(`<input />`, () => h('input', { readonly: false }))
  1516. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1517. mountWithHydration(`<input readonly />`, () =>
  1518. h('input', { readonly: true }),
  1519. )
  1520. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1521. mountWithHydration(`<input readonly="readonly" />`, () =>
  1522. h('input', { readonly: true }),
  1523. )
  1524. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1525. })
  1526. test('client value is null or undefined', () => {
  1527. mountWithHydration(`<div></div>`, () =>
  1528. h('div', { draggable: undefined }),
  1529. )
  1530. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1531. mountWithHydration(`<input />`, () => h('input', { type: null }))
  1532. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1533. })
  1534. test('should not warn against object values', () => {
  1535. mountWithHydration(`<input />`, () => h('input', { from: {} }))
  1536. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1537. })
  1538. test('should not warn on falsy bindings of non-property keys', () => {
  1539. mountWithHydration(`<button />`, () => h('button', { href: undefined }))
  1540. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1541. })
  1542. test('should not warn on non-renderable option values', () => {
  1543. mountWithHydration(`<select><option>hello</option></select>`, () =>
  1544. h('select', [h('option', { value: ['foo'] }, 'hello')]),
  1545. )
  1546. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1547. })
  1548. test('should not warn css v-bind', () => {
  1549. const container = document.createElement('div')
  1550. container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
  1551. const app = createSSRApp({
  1552. setup() {
  1553. useCssVars(() => ({
  1554. foo: 'red',
  1555. }))
  1556. return () => h('div', { style: { color: 'var(--foo)' } })
  1557. },
  1558. })
  1559. app.mount(container)
  1560. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  1561. })
  1562. // #10317 - test case from #10325
  1563. test('css vars should only be added to expected on component root dom', () => {
  1564. const container = document.createElement('div')
  1565. container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
  1566. const app = createSSRApp({
  1567. setup() {
  1568. useCssVars(() => ({
  1569. foo: 'red',
  1570. }))
  1571. return () =>
  1572. h('div', null, [h('div', { style: { color: 'var(--foo)' } })])
  1573. },
  1574. })
  1575. app.mount(container)
  1576. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  1577. })
  1578. // #11188
  1579. test('css vars support fallthrough', () => {
  1580. const container = document.createElement('div')
  1581. container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
  1582. const app = createSSRApp({
  1583. setup() {
  1584. useCssVars(() => ({
  1585. foo: 'red',
  1586. }))
  1587. return () => h(Child)
  1588. },
  1589. })
  1590. const Child = {
  1591. setup() {
  1592. return () => h('div', { style: 'padding: 4px' })
  1593. },
  1594. }
  1595. app.mount(container)
  1596. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  1597. })
  1598. // #11189
  1599. test('should not warn for directives that mutate DOM in created', () => {
  1600. const container = document.createElement('div')
  1601. container.innerHTML = `<div class="test red"></div>`
  1602. const vColor: ObjectDirective = {
  1603. created(el, binding) {
  1604. el.classList.add(binding.value)
  1605. },
  1606. }
  1607. const app = createSSRApp({
  1608. setup() {
  1609. return () =>
  1610. withDirectives(h('div', { class: 'test' }), [[vColor, 'red']])
  1611. },
  1612. })
  1613. app.mount(container)
  1614. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  1615. })
  1616. })
  1617. describe('data-allow-mismatch', () => {
  1618. test('element text content', () => {
  1619. const { container } = mountWithHydration(
  1620. `<div data-allow-mismatch="text">foo</div>`,
  1621. () => h('div', 'bar'),
  1622. )
  1623. expect(container.innerHTML).toBe(
  1624. '<div data-allow-mismatch="text">bar</div>',
  1625. )
  1626. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  1627. })
  1628. test('not enough children', () => {
  1629. const { container } = mountWithHydration(
  1630. `<div data-allow-mismatch="children"></div>`,
  1631. () => h('div', [h('span', 'foo'), h('span', 'bar')]),
  1632. )
  1633. expect(container.innerHTML).toBe(
  1634. '<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>',
  1635. )
  1636. expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  1637. })
  1638. test('too many children', () => {
  1639. const { container } = mountWithHydration(
  1640. `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
  1641. () => h('div', [h('span', 'foo')]),
  1642. )
  1643. expect(container.innerHTML).toBe(
  1644. '<div data-allow-mismatch="children"><span>foo</span></div>',
  1645. )
  1646. expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  1647. })
  1648. test('complete mismatch', () => {
  1649. const { container } = mountWithHydration(
  1650. `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
  1651. () => h('div', [h('div', 'foo'), h('p', 'bar')]),
  1652. )
  1653. expect(container.innerHTML).toBe(
  1654. '<div data-allow-mismatch="children"><div>foo</div><p>bar</p></div>',
  1655. )
  1656. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  1657. })
  1658. test('fragment mismatch removal', () => {
  1659. const { container } = mountWithHydration(
  1660. `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  1661. () => h('div', [h('span', 'replaced')]),
  1662. )
  1663. expect(container.innerHTML).toBe(
  1664. '<div data-allow-mismatch="children"><span>replaced</span></div>',
  1665. )
  1666. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  1667. })
  1668. test('fragment not enough children', () => {
  1669. const { container } = mountWithHydration(
  1670. `<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  1671. () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
  1672. )
  1673. expect(container.innerHTML).toBe(
  1674. '<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
  1675. )
  1676. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  1677. })
  1678. test('fragment too many children', () => {
  1679. const { container } = mountWithHydration(
  1680. `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  1681. () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
  1682. )
  1683. expect(container.innerHTML).toBe(
  1684. '<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>',
  1685. )
  1686. // fragment ends early and attempts to hydrate the extra <div>bar</div>
  1687. // as 2nd fragment child.
  1688. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  1689. // excessive children removal
  1690. expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  1691. })
  1692. test('comment mismatch (element)', () => {
  1693. const { container } = mountWithHydration(
  1694. `<div data-allow-mismatch="children"><span></span></div>`,
  1695. () => h('div', [createCommentVNode('hi')]),
  1696. )
  1697. expect(container.innerHTML).toBe(
  1698. '<div data-allow-mismatch="children"><!--hi--></div>',
  1699. )
  1700. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  1701. })
  1702. test('comment mismatch (text)', () => {
  1703. const { container } = mountWithHydration(
  1704. `<div data-allow-mismatch="children">foobar</div>`,
  1705. () => h('div', [createCommentVNode('hi')]),
  1706. )
  1707. expect(container.innerHTML).toBe(
  1708. '<div data-allow-mismatch="children"><!--hi--></div>',
  1709. )
  1710. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  1711. })
  1712. test('class mismatch', () => {
  1713. mountWithHydration(
  1714. `<div class="foo bar" data-allow-mismatch="class"></div>`,
  1715. () => h('div', { class: 'foo' }),
  1716. )
  1717. expect(`Hydration class mismatch`).not.toHaveBeenWarned()
  1718. })
  1719. test('style mismatch', () => {
  1720. mountWithHydration(
  1721. `<div style="color:red;" data-allow-mismatch="style"></div>`,
  1722. () => h('div', { style: { color: 'green' } }),
  1723. )
  1724. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  1725. })
  1726. test('attr mismatch', () => {
  1727. mountWithHydration(`<div data-allow-mismatch="attribute"></div>`, () =>
  1728. h('div', { id: 'foo' }),
  1729. )
  1730. mountWithHydration(
  1731. `<div id="bar" data-allow-mismatch="attribute"></div>`,
  1732. () => h('div', { id: 'foo' }),
  1733. )
  1734. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  1735. })
  1736. })
  1737. })