hydration.spec.ts 126 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486
  1. import { createVaporSSRApp, delegateEvents } from '../src'
  2. import { nextTick, reactive, ref } from '@vue/runtime-dom'
  3. import { compileScript, parse } from '@vue/compiler-sfc'
  4. import * as runtimeVapor from '../src'
  5. import * as runtimeDom from '@vue/runtime-dom'
  6. import * as VueServerRenderer from '@vue/server-renderer'
  7. import { isString } from '@vue/shared'
  8. import type { VaporComponentInstance } from '../src/component'
  9. import type { TeleportFragment } from '../src/components/Teleport'
  10. const formatHtml = (raw: string) => {
  11. return raw
  12. .replace(/<!--\[/g, '\n<!--[')
  13. .replace(/]-->/g, ']-->\n')
  14. .replace(/\n{2,}/g, '\n')
  15. }
  16. const Vue = { ...runtimeDom, ...runtimeVapor }
  17. function compile(
  18. sfc: string,
  19. data: runtimeDom.Ref<any>,
  20. components: Record<string, any> = {},
  21. { vapor = true, ssr = false } = {},
  22. ) {
  23. if (!sfc.includes(`<script`)) {
  24. sfc =
  25. `<script vapor>const data = _data; const components = _components;</script>` +
  26. sfc
  27. }
  28. const descriptor = parse(sfc).descriptor
  29. const script = compileScript(descriptor, {
  30. id: 'x',
  31. isProd: true,
  32. inlineTemplate: true,
  33. genDefaultAs: '__sfc__',
  34. vapor,
  35. templateOptions: {
  36. ssr,
  37. },
  38. })
  39. const code =
  40. script.content
  41. .replace(/\bimport {/g, 'const {')
  42. .replace(/ as _/g, ': _')
  43. .replace(/} from ['"]vue['"]/g, `} = Vue`)
  44. .replace(/} from "vue\/server-renderer"/g, '} = VueServerRenderer') +
  45. '\nreturn __sfc__'
  46. return new Function('Vue', 'VueServerRenderer', '_data', '_components', code)(
  47. Vue,
  48. VueServerRenderer,
  49. data,
  50. components,
  51. )
  52. }
  53. async function testWithVaporApp(
  54. code: string,
  55. components?: Record<string, string | { code: string; vapor: boolean }>,
  56. data?: any,
  57. ) {
  58. return testHydration(code, components, data, {
  59. isVaporApp: true,
  60. interop: true,
  61. })
  62. }
  63. async function testWithVDOMApp(
  64. code: string,
  65. components?: Record<string, string | { code: string; vapor: boolean }>,
  66. data?: any,
  67. ) {
  68. return testHydration(code, components, data, {
  69. isVaporApp: false,
  70. interop: true,
  71. })
  72. }
  73. function compileVaporComponent(
  74. code: string,
  75. data: runtimeDom.Ref<any> = ref({}),
  76. components?: Record<string, any>,
  77. ssr = false,
  78. ) {
  79. return compile(`<template>${code}</template>`, data, components, {
  80. vapor: true,
  81. ssr,
  82. })
  83. }
  84. async function mountWithHydration(
  85. html: string,
  86. code: string,
  87. data: runtimeDom.Ref<any> = ref({}),
  88. components?: Record<string, any>,
  89. ) {
  90. const container = document.createElement('div')
  91. container.innerHTML = html
  92. document.body.appendChild(container)
  93. const clientComp = compileVaporComponent(code, data, components)
  94. const app = createVaporSSRApp(clientComp)
  95. app.mount(container)
  96. return {
  97. block: (app._instance! as VaporComponentInstance).block,
  98. container,
  99. }
  100. }
  101. async function testHydration(
  102. code: string,
  103. components: Record<string, string | { code: string; vapor: boolean }> = {},
  104. data: any = ref('foo'),
  105. { isVaporApp = true, interop = false } = {},
  106. ) {
  107. const ssrComponents: any = {}
  108. const clientComponents: any = {}
  109. for (const key in components) {
  110. const comp = components[key]
  111. const code = isString(comp) ? comp : comp.code
  112. const isVaporComp = isString(comp) || !!comp.vapor
  113. clientComponents[key] = compile(code, data, clientComponents, {
  114. vapor: isVaporComp,
  115. ssr: false,
  116. })
  117. ssrComponents[key] = compile(code, data, ssrComponents, {
  118. vapor: isVaporComp,
  119. ssr: true,
  120. })
  121. }
  122. const serverComp = compile(code, data, ssrComponents, {
  123. vapor: isVaporApp,
  124. ssr: true,
  125. })
  126. const html = await VueServerRenderer.renderToString(
  127. runtimeDom.createSSRApp(serverComp),
  128. )
  129. const container = document.createElement('div')
  130. document.body.appendChild(container)
  131. container.innerHTML = html
  132. const clientComp = compile(code, data, clientComponents, {
  133. vapor: isVaporApp,
  134. ssr: false,
  135. })
  136. let app
  137. if (isVaporApp) {
  138. app = createVaporSSRApp(clientComp)
  139. } else {
  140. app = runtimeDom.createSSRApp(clientComp)
  141. }
  142. if (interop) {
  143. app.use(runtimeVapor.vaporInteropPlugin)
  144. }
  145. app.mount(container)
  146. return { data, container }
  147. }
  148. const triggerEvent = (type: string, el: Element) => {
  149. const event = new Event(type, { bubbles: true })
  150. el.dispatchEvent(event)
  151. }
  152. delegateEvents('click')
  153. beforeEach(() => {
  154. document.body.innerHTML = ''
  155. })
  156. describe('Vapor Mode hydration', () => {
  157. describe('text', () => {
  158. test('root text', async () => {
  159. const { data, container } = await testHydration(`
  160. <template>{{ data }}</template>
  161. `)
  162. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"foo"`)
  163. data.value = 'bar'
  164. await nextTick()
  165. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"bar"`)
  166. })
  167. test('consecutive text nodes', async () => {
  168. const { data, container } = await testHydration(`
  169. <template>{{ data }}{{ data }}</template>
  170. `)
  171. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"foofoo"`)
  172. data.value = 'bar'
  173. await nextTick()
  174. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"barbar"`)
  175. })
  176. test('consecutive text nodes with insertion anchor', async () => {
  177. const { data, container } = await testHydration(`
  178. <template><span/>{{ data }}{{ data }}<span/></template>
  179. `)
  180. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  181. `
  182. "
  183. <!--[--><span></span>foofoo<span></span><!--]-->
  184. "
  185. `,
  186. )
  187. data.value = 'bar'
  188. await nextTick()
  189. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  190. `
  191. "
  192. <!--[--><span></span>barbar<span></span><!--]-->
  193. "
  194. `,
  195. )
  196. })
  197. test('mixed text nodes', async () => {
  198. const { data, container } = await testHydration(`
  199. <template>{{ data }}A{{ data }}B{{ data }}</template>
  200. `)
  201. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  202. `"fooAfooBfoo"`,
  203. )
  204. data.value = 'bar'
  205. await nextTick()
  206. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  207. `"barAbarBbar"`,
  208. )
  209. })
  210. test('mixed text nodes with insertion anchor', async () => {
  211. const { data, container } = await testHydration(`
  212. <template><span/>{{ data }}A{{ data }}B{{ data }}<span/></template>
  213. `)
  214. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  215. `
  216. "
  217. <!--[--><span></span>fooAfooBfoo<span></span><!--]-->
  218. "
  219. `,
  220. )
  221. data.value = 'bar'
  222. await nextTick()
  223. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  224. `
  225. "
  226. <!--[--><span></span>barAbarBbar<span></span><!--]-->
  227. "
  228. `,
  229. )
  230. })
  231. test('empty text node', async () => {
  232. const data = reactive({ txt: '' })
  233. const { container } = await testHydration(
  234. `<template><div>{{ data.txt }}</div></template>`,
  235. undefined,
  236. data,
  237. )
  238. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  239. `"<div></div>"`,
  240. )
  241. data.txt = 'foo'
  242. await nextTick()
  243. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  244. `"<div>foo</div>"`,
  245. )
  246. })
  247. test('empty text node in slot', async () => {
  248. const data = reactive({ txt: '' })
  249. const { container } = await testHydration(
  250. `<template><components.Child>{{data.txt}}</components.Child></template>`,
  251. {
  252. Child: `<template><slot/></template>`,
  253. },
  254. data,
  255. )
  256. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  257. `
  258. "
  259. <!--[--><!--]-->
  260. "
  261. `,
  262. )
  263. data.txt = 'foo'
  264. await nextTick()
  265. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  266. `
  267. "
  268. <!--[-->foo<!--]-->
  269. "
  270. `,
  271. )
  272. })
  273. })
  274. describe('element', () => {
  275. test('root comment', async () => {
  276. const { container } = await testHydration(`
  277. <template><!----></template>
  278. `)
  279. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"<!---->"`)
  280. expect(`mismatch in <div>`).not.toHaveBeenWarned()
  281. })
  282. test('root with mixed element and text', async () => {
  283. const { container, data } = await testHydration(`
  284. <template> A<span>{{ data }}</span>{{ data }}</template>
  285. `)
  286. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  287. `
  288. "
  289. <!--[--> A<span>foo</span>foo<!--]-->
  290. "
  291. `,
  292. )
  293. data.value = 'bar'
  294. await nextTick()
  295. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  296. `
  297. "
  298. <!--[--> A<span>bar</span>bar<!--]-->
  299. "
  300. `,
  301. )
  302. })
  303. test('empty element', async () => {
  304. const { container } = await testHydration(`
  305. <template><div/></template>
  306. `)
  307. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  308. `"<div></div>"`,
  309. )
  310. expect(`mismatch in <div>`).not.toHaveBeenWarned()
  311. })
  312. test('element with binding and text children', async () => {
  313. const { container, data } = await testHydration(`
  314. <template><div :class="data">{{ data }}</div></template>
  315. `)
  316. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  317. `"<div class="foo">foo</div>"`,
  318. )
  319. data.value = 'bar'
  320. await nextTick()
  321. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  322. `"<div class="bar">bar</div>"`,
  323. )
  324. })
  325. test('element with elements children', async () => {
  326. const { container } = await testHydration(`
  327. <template>
  328. <div>
  329. <span>{{ data }}</span>
  330. <span :class="data" @click="data = 'bar'"/>
  331. </div>
  332. </template>
  333. `)
  334. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  335. `"<div><span>foo</span><span class="foo"></span></div>"`,
  336. )
  337. // event handler
  338. triggerEvent('click', container.querySelector('.foo')!)
  339. await nextTick()
  340. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  341. `"<div><span>bar</span><span class="bar"></span></div>"`,
  342. )
  343. })
  344. test('element with ref', async () => {
  345. const { data, container } = await testHydration(
  346. `<template>
  347. <div ref="data">hi</div>
  348. </template>
  349. `,
  350. {},
  351. ref(null),
  352. )
  353. expect(data.value).toBe(container.firstChild)
  354. })
  355. })
  356. describe('component', () => {
  357. test('basic component', async () => {
  358. const { container, data } = await testHydration(
  359. `
  360. <template><div><span></span><components.Child/></div></template>
  361. `,
  362. { Child: `<template>{{ data }}</template>` },
  363. )
  364. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  365. `"<div><span></span>foo</div>"`,
  366. )
  367. data.value = 'bar'
  368. await nextTick()
  369. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  370. `"<div><span></span>bar</div>"`,
  371. )
  372. })
  373. test('fragment component', async () => {
  374. const { container, data } = await testHydration(
  375. `
  376. <template><div><span></span><components.Child/></div></template>
  377. `,
  378. { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
  379. )
  380. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  381. `
  382. "<div><span></span>
  383. <!--[--><div>foo</div>-foo-<!--]-->
  384. </div>"
  385. `,
  386. )
  387. data.value = 'bar'
  388. await nextTick()
  389. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  390. `
  391. "<div><span></span>
  392. <!--[--><div>bar</div>-bar-<!--]-->
  393. </div>"
  394. `,
  395. )
  396. })
  397. test('fragment component with prepend', async () => {
  398. const { container, data } = await testHydration(
  399. `
  400. <template><div><components.Child/><span></span></div></template>
  401. `,
  402. { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
  403. )
  404. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  405. `
  406. "<div>
  407. <!--[--><div>foo</div>-foo-<!--]-->
  408. <span></span></div>"
  409. `,
  410. )
  411. data.value = 'bar'
  412. await nextTick()
  413. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  414. `
  415. "<div>
  416. <!--[--><div>bar</div>-bar-<!--]-->
  417. <span></span></div>"
  418. `,
  419. )
  420. })
  421. test('nested fragment components', async () => {
  422. const { container, data } = await testHydration(
  423. `
  424. <template><div><components.Parent/><span></span></div></template>
  425. `,
  426. {
  427. Parent: `<template><div/><components.Child/><div/></template>`,
  428. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  429. },
  430. )
  431. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  432. `
  433. "<div>
  434. <!--[--><div></div>
  435. <!--[--><div>foo</div>-foo-<!--]-->
  436. <div></div><!--]-->
  437. <span></span></div>"
  438. `,
  439. )
  440. data.value = 'bar'
  441. await nextTick()
  442. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  443. `
  444. "<div>
  445. <!--[--><div></div>
  446. <!--[--><div>bar</div>-bar-<!--]-->
  447. <div></div><!--]-->
  448. <span></span></div>"
  449. `,
  450. )
  451. })
  452. test('component with insertion anchor', async () => {
  453. const { container, data } = await testHydration(
  454. `<template>
  455. <div>
  456. <span/>
  457. <components.Child/>
  458. <span/>
  459. </div>
  460. </template>
  461. `,
  462. {
  463. Child: `<template>{{ data }}</template>`,
  464. },
  465. )
  466. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  467. `"<div><span></span>foo<span></span></div>"`,
  468. )
  469. data.value = 'bar'
  470. await nextTick()
  471. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  472. `"<div><span></span>bar<span></span></div>"`,
  473. )
  474. })
  475. test('nested components with insertion anchor', async () => {
  476. const { container, data } = await testHydration(
  477. `
  478. <template><components.Parent/></template>
  479. `,
  480. {
  481. Parent: `<template><div><span/><components.Child/><span/></div></template>`,
  482. Child: `<template><div>{{ data }}</div></template>`,
  483. },
  484. )
  485. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  486. `"<div><span></span><div>foo</div><span></span></div>"`,
  487. )
  488. data.value = 'bar'
  489. await nextTick()
  490. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  491. `"<div><span></span><div>bar</div><span></span></div>"`,
  492. )
  493. })
  494. test('nested components with multi level anchor insertion', async () => {
  495. const { container, data } = await testHydration(
  496. `
  497. <template><div><span></span><components.Parent/><span></span></div></template>
  498. `,
  499. {
  500. Parent: `<template><div><span/><components.Child/><span/></div></template>`,
  501. Child: `<template><div>{{ data }}</div></template>`,
  502. },
  503. )
  504. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  505. `"<div><span></span><div><span></span><div>foo</div><span></span></div><span></span></div>"`,
  506. )
  507. data.value = 'bar'
  508. await nextTick()
  509. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  510. `"<div><span></span><div><span></span><div>bar</div><span></span></div><span></span></div>"`,
  511. )
  512. })
  513. test('consecutive components with insertion parent', async () => {
  514. const data = reactive({ foo: 'foo', bar: 'bar' })
  515. const { container } = await testHydration(
  516. `<template>
  517. <div>
  518. <components.Child1/>
  519. <components.Child2/>
  520. </div>
  521. </template>
  522. `,
  523. {
  524. Child1: `<template><span>{{ data.foo }}</span></template>`,
  525. Child2: `<template><span>{{ data.bar }}</span></template>`,
  526. },
  527. data,
  528. )
  529. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  530. `"<div><span>foo</span><span>bar</span></div>"`,
  531. )
  532. data.foo = 'foo1'
  533. data.bar = 'bar1'
  534. await nextTick()
  535. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  536. `"<div><span>foo1</span><span>bar1</span></div>"`,
  537. )
  538. })
  539. test('nested consecutive components with insertion anchor', async () => {
  540. const { container, data } = await testHydration(
  541. `
  542. <template><components.Parent/></template>
  543. `,
  544. {
  545. Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
  546. Child: `<template><div>{{ data }}</div></template>`,
  547. },
  548. )
  549. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  550. `"<div><span></span><div>foo</div><div>foo</div><span></span></div>"`,
  551. )
  552. data.value = 'bar'
  553. await nextTick()
  554. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  555. `"<div><span></span><div>bar</div><div>bar</div><span></span></div>"`,
  556. )
  557. })
  558. test('nested consecutive components with multi level anchor insertion', async () => {
  559. const { container, data } = await testHydration(
  560. `
  561. <template><div><span></span><components.Parent/><span></span></div></template>
  562. `,
  563. {
  564. Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
  565. Child: `<template><div>{{ data }}</div></template>`,
  566. },
  567. )
  568. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  569. `"<div><span></span><div><span></span><div>foo</div><div>foo</div><span></span></div><span></span></div>"`,
  570. )
  571. data.value = 'bar'
  572. await nextTick()
  573. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  574. `"<div><span></span><div><span></span><div>bar</div><div>bar</div><span></span></div><span></span></div>"`,
  575. )
  576. })
  577. test('mixed component and element with insertion anchor', async () => {
  578. const { container, data } = await testHydration(
  579. `<template>
  580. <div>
  581. <span/>
  582. <components.Child/>
  583. <span/>
  584. <components.Child/>
  585. <span/>
  586. </div>
  587. </template>
  588. `,
  589. {
  590. Child: `<template>{{ data }}</template>`,
  591. },
  592. )
  593. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  594. `"<div><span></span>foo<span></span>foo<span></span></div>"`,
  595. )
  596. data.value = 'bar'
  597. await nextTick()
  598. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  599. `"<div><span></span>bar<span></span>bar<span></span></div>"`,
  600. )
  601. })
  602. test('fragment component with insertion anchor', async () => {
  603. const { container, data } = await testHydration(
  604. `<template>
  605. <div>
  606. <span/>
  607. <components.Child/>
  608. <span/>
  609. </div>
  610. </template>
  611. `,
  612. {
  613. Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
  614. },
  615. )
  616. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  617. `
  618. "<div><span></span>
  619. <!--[--><div>foo</div>-foo<!--]-->
  620. <span></span></div>"
  621. `,
  622. )
  623. data.value = 'bar'
  624. await nextTick()
  625. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  626. `
  627. "<div><span></span>
  628. <!--[--><div>bar</div>-bar<!--]-->
  629. <span></span></div>"
  630. `,
  631. )
  632. })
  633. test('nested fragment component with insertion anchor', async () => {
  634. const { container, data } = await testHydration(
  635. `
  636. <template><components.Parent/></template>
  637. `,
  638. {
  639. Parent: `<template><div><span/><components.Child/><span/></div></template>`,
  640. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  641. },
  642. )
  643. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  644. `
  645. "<div><span></span>
  646. <!--[--><div>foo</div>-foo-<!--]-->
  647. <span></span></div>"
  648. `,
  649. )
  650. data.value = 'bar'
  651. await nextTick()
  652. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  653. `
  654. "<div><span></span>
  655. <!--[--><div>bar</div>-bar-<!--]-->
  656. <span></span></div>"
  657. `,
  658. )
  659. })
  660. test('nested fragment component with multi level anchor insertion', async () => {
  661. const { container, data } = await testHydration(
  662. `
  663. <template><div><span/><components.Parent/><span/></div></template>
  664. `,
  665. {
  666. Parent: `<template><div><span/><components.Child/><span/></div></template>`,
  667. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  668. },
  669. )
  670. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  671. `
  672. "<div><span></span><div><span></span>
  673. <!--[--><div>foo</div>-foo-<!--]-->
  674. <span></span></div><span></span></div>"
  675. `,
  676. )
  677. data.value = 'bar'
  678. await nextTick()
  679. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  680. `
  681. "<div><span></span><div><span></span>
  682. <!--[--><div>bar</div>-bar-<!--]-->
  683. <span></span></div><span></span></div>"
  684. `,
  685. )
  686. })
  687. test('consecutive fragment components with insertion anchor', async () => {
  688. const { container, data } = await testHydration(
  689. `<template>
  690. <div>
  691. <span/>
  692. <components.Child/>
  693. <components.Child/>
  694. <span/>
  695. </div>
  696. </template>
  697. `,
  698. {
  699. Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
  700. },
  701. )
  702. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  703. `
  704. "<div><span></span>
  705. <!--[--><div>foo</div>-foo<!--]-->
  706. <!--[--><div>foo</div>-foo<!--]-->
  707. <span></span></div>"
  708. `,
  709. )
  710. data.value = 'bar'
  711. await nextTick()
  712. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  713. `
  714. "<div><span></span>
  715. <!--[--><div>bar</div>-bar<!--]-->
  716. <!--[--><div>bar</div>-bar<!--]-->
  717. <span></span></div>"
  718. `,
  719. )
  720. })
  721. test('nested consecutive fragment components with insertion anchor', async () => {
  722. const { container, data } = await testHydration(
  723. `
  724. <template><components.Parent/></template>
  725. `,
  726. {
  727. Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
  728. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  729. },
  730. )
  731. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  732. `
  733. "<div><span></span>
  734. <!--[--><div>foo</div>-foo-<!--]-->
  735. <!--[--><div>foo</div>-foo-<!--]-->
  736. <span></span></div>"
  737. `,
  738. )
  739. data.value = 'bar'
  740. await nextTick()
  741. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  742. `
  743. "<div><span></span>
  744. <!--[--><div>bar</div>-bar-<!--]-->
  745. <!--[--><div>bar</div>-bar-<!--]-->
  746. <span></span></div>"
  747. `,
  748. )
  749. })
  750. test('nested consecutive fragment components with multi level anchor insertion', async () => {
  751. const { container, data } = await testHydration(
  752. `
  753. <template><div><span></span><components.Parent/><span></span></div></template>
  754. `,
  755. {
  756. Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
  757. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  758. },
  759. )
  760. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  761. `
  762. "<div><span></span><div><span></span>
  763. <!--[--><div>foo</div>-foo-<!--]-->
  764. <!--[--><div>foo</div>-foo-<!--]-->
  765. <span></span></div><span></span></div>"
  766. `,
  767. )
  768. data.value = 'bar'
  769. await nextTick()
  770. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  771. `
  772. "<div><span></span><div><span></span>
  773. <!--[--><div>bar</div>-bar-<!--]-->
  774. <!--[--><div>bar</div>-bar-<!--]-->
  775. <span></span></div><span></span></div>"
  776. `,
  777. )
  778. })
  779. test('nested consecutive fragment components with root level anchor insertion', async () => {
  780. const { container, data } = await testHydration(
  781. `
  782. <template><div><span></span><components.Parent/><span></span></div></template>
  783. `,
  784. {
  785. Parent: `<template><components.Child/><components.Child/></template>`,
  786. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  787. },
  788. )
  789. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  790. `
  791. "<div><span></span>
  792. <!--[-->
  793. <!--[--><div>foo</div>-foo-<!--]-->
  794. <!--[--><div>foo</div>-foo-<!--]-->
  795. <!--]-->
  796. <span></span></div>"
  797. `,
  798. )
  799. data.value = 'bar'
  800. await nextTick()
  801. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  802. `
  803. "<div><span></span>
  804. <!--[-->
  805. <!--[--><div>bar</div>-bar-<!--]-->
  806. <!--[--><div>bar</div>-bar-<!--]-->
  807. <!--]-->
  808. <span></span></div>"
  809. `,
  810. )
  811. })
  812. test('mixed fragment component and element with insertion anchor', async () => {
  813. const { container, data } = await testHydration(
  814. `<template>
  815. <div>
  816. <span/>
  817. <components.Child/>
  818. <span/>
  819. <components.Child/>
  820. <span/>
  821. </div>
  822. </template>
  823. `,
  824. {
  825. Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
  826. },
  827. )
  828. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  829. `
  830. "<div><span></span>
  831. <!--[--><div>foo</div>-foo<!--]-->
  832. <span></span>
  833. <!--[--><div>foo</div>-foo<!--]-->
  834. <span></span></div>"
  835. `,
  836. )
  837. data.value = 'bar'
  838. await nextTick()
  839. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  840. `
  841. "<div><span></span>
  842. <!--[--><div>bar</div>-bar<!--]-->
  843. <span></span>
  844. <!--[--><div>bar</div>-bar<!--]-->
  845. <span></span></div>"
  846. `,
  847. )
  848. })
  849. test('mixed fragment component and text with insertion anchor', async () => {
  850. const { container, data } = await testHydration(
  851. `<template>
  852. <div>
  853. <span/>
  854. <components.Child/>
  855. {{ data }}
  856. <components.Child/>
  857. <span/>
  858. </div>
  859. </template>
  860. `,
  861. {
  862. Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
  863. },
  864. )
  865. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  866. `
  867. "<div><span></span>
  868. <!--[--><div>foo</div>-foo<!--]-->
  869. foo
  870. <!--[--><div>foo</div>-foo<!--]-->
  871. <span></span></div>"
  872. `,
  873. )
  874. data.value = 'bar'
  875. await nextTick()
  876. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  877. `
  878. "<div><span></span>
  879. <!--[--><div>bar</div>-bar<!--]-->
  880. bar
  881. <!--[--><div>bar</div>-bar<!--]-->
  882. <span></span></div>"
  883. `,
  884. )
  885. })
  886. })
  887. describe('dynamic component', () => {
  888. test('basic dynamic component', async () => {
  889. const { container, data } = await testHydration(
  890. `<template>
  891. <component :is="components[data]"/>
  892. </template>`,
  893. {
  894. foo: `<template><div>foo</div></template>`,
  895. bar: `<template><div>bar</div></template>`,
  896. },
  897. ref('foo'),
  898. )
  899. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  900. `"<div>foo</div><!--dynamic-component-->"`,
  901. )
  902. data.value = 'bar'
  903. await nextTick()
  904. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  905. `"<div>bar</div><!--dynamic-component-->"`,
  906. )
  907. })
  908. test('dynamic component with insertion anchor', async () => {
  909. const { container, data } = await testHydration(
  910. `<template>
  911. <div>
  912. <span/>
  913. <component :is="components[data]"/>
  914. <span/>
  915. </div>
  916. </template>`,
  917. {
  918. foo: `<template><div>foo</div></template>`,
  919. bar: `<template><div>bar</div></template>`,
  920. },
  921. ref('foo'),
  922. )
  923. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  924. `"<div><span></span><div>foo</div><!--dynamic-component--><span></span></div>"`,
  925. )
  926. data.value = 'bar'
  927. await nextTick()
  928. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  929. `"<div><span></span><div>bar</div><!--dynamic-component--><span></span></div>"`,
  930. )
  931. })
  932. test('consecutive dynamic components with insertion anchor', async () => {
  933. const { container, data } = await testHydration(
  934. `<template>
  935. <div>
  936. <span/>
  937. <component :is="components[data]"/>
  938. <component :is="components[data]"/>
  939. <span/>
  940. </div>
  941. </template>`,
  942. {
  943. foo: `<template><div>foo</div></template>`,
  944. bar: `<template><div>bar</div></template>`,
  945. },
  946. ref('foo'),
  947. )
  948. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  949. `"<div><span></span><div>foo</div><!--dynamic-component--><div>foo</div><!--dynamic-component--><span></span></div>"`,
  950. )
  951. data.value = 'bar'
  952. await nextTick()
  953. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  954. `"<div><span></span><div>bar</div><!--dynamic-component--><div>bar</div><!--dynamic-component--><span></span></div>"`,
  955. )
  956. })
  957. test('dynamic component fallback', async () => {
  958. const { container, data } = await testHydration(
  959. `<template>
  960. <component :is="'button'">
  961. <span>{{ data }}</span>
  962. </component>
  963. </template>`,
  964. {},
  965. ref('foo'),
  966. )
  967. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  968. `"<button><span>foo</span></button><!--dynamic-component-->"`,
  969. )
  970. data.value = 'bar'
  971. await nextTick()
  972. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  973. `"<button><span>bar</span></button><!--dynamic-component-->"`,
  974. )
  975. })
  976. test('in ssr slot vnode fallback', async () => {
  977. const { container, data } = await testHydration(
  978. `<template>
  979. <components.Child>
  980. <span>{{ data }}</span>
  981. </components.Child>
  982. </template>`,
  983. {
  984. Child: `
  985. <template>
  986. <component :is="'div'">
  987. <slot />
  988. </component>
  989. </template>`,
  990. },
  991. ref('foo'),
  992. )
  993. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  994. `
  995. "<div>
  996. <!--[--><span>foo</span><!--]-->
  997. </div><!--dynamic-component-->"
  998. `,
  999. )
  1000. data.value = 'bar'
  1001. await nextTick()
  1002. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1003. `
  1004. "<div>
  1005. <!--[--><span>bar</span><!--]-->
  1006. </div><!--dynamic-component-->"
  1007. `,
  1008. )
  1009. })
  1010. })
  1011. describe('if', () => {
  1012. test('basic toggle - true -> false', async () => {
  1013. const data = ref(true)
  1014. const { container } = await testHydration(
  1015. `<template>
  1016. <div v-if="data">foo</div>
  1017. </template>`,
  1018. undefined,
  1019. data,
  1020. )
  1021. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1022. `"<div>foo</div><!--if-->"`,
  1023. )
  1024. data.value = false
  1025. await nextTick()
  1026. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1027. `"<!--if-->"`,
  1028. )
  1029. })
  1030. test('basic toggle - false -> true', async () => {
  1031. const data = ref(false)
  1032. const { container } = await testHydration(
  1033. `<template>
  1034. <div v-if="data">foo</div>
  1035. </template>`,
  1036. undefined,
  1037. data,
  1038. )
  1039. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1040. `"<!--if-->"`,
  1041. )
  1042. data.value = true
  1043. await nextTick()
  1044. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1045. `"<div>foo</div><!--if-->"`,
  1046. )
  1047. })
  1048. test('v-if on insertion parent', async () => {
  1049. const data = ref(true)
  1050. const { container } = await testHydration(
  1051. `<template>
  1052. <div v-if="data">
  1053. <components.Child/>
  1054. </div>
  1055. </template>`,
  1056. { Child: `<template>foo</template>` },
  1057. data,
  1058. )
  1059. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1060. `"<div>foo</div><!--if-->"`,
  1061. )
  1062. data.value = false
  1063. await nextTick()
  1064. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1065. `"<!--if-->"`,
  1066. )
  1067. data.value = true
  1068. await nextTick()
  1069. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1070. `"<div>foo</div><!--if-->"`,
  1071. )
  1072. })
  1073. test('v-if/else-if/else chain - switch branches', async () => {
  1074. const data = ref('a')
  1075. const { container } = await testHydration(
  1076. `<template>
  1077. <div v-if="data === 'a'">foo</div>
  1078. <div v-else-if="data === 'b'">bar</div>
  1079. <div v-else>baz</div>
  1080. </template>`,
  1081. undefined,
  1082. data,
  1083. )
  1084. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1085. `"<div>foo</div><!--if-->"`,
  1086. )
  1087. data.value = 'b'
  1088. await nextTick()
  1089. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1090. `"<div>bar</div><!--if--><!--if-->"`,
  1091. )
  1092. data.value = 'c'
  1093. await nextTick()
  1094. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1095. `"<div>baz</div><!--if--><!--if-->"`,
  1096. )
  1097. data.value = 'a'
  1098. await nextTick()
  1099. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1100. `"<div>foo</div><!--if-->"`,
  1101. )
  1102. })
  1103. test('nested if', async () => {
  1104. const data = reactive({ outer: true, inner: true })
  1105. const { container } = await testHydration(
  1106. `<template>
  1107. <div v-if="data.outer">
  1108. <span>outer</span>
  1109. <div v-if="data.inner">inner</div>
  1110. </div>
  1111. </template>`,
  1112. undefined,
  1113. data,
  1114. )
  1115. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1116. `"<div><span>outer</span><div>inner</div><!--if--></div><!--if-->"`,
  1117. )
  1118. data.inner = false
  1119. await nextTick()
  1120. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1121. `"<div><span>outer</span><!--if--></div><!--if-->"`,
  1122. )
  1123. data.outer = false
  1124. await nextTick()
  1125. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1126. `"<!--if-->"`,
  1127. )
  1128. })
  1129. test('on component', async () => {
  1130. const data = ref(true)
  1131. const { container } = await testHydration(
  1132. `<template>
  1133. <components.Child v-if="data"/>
  1134. </template>`,
  1135. { Child: `<template>foo</template>` },
  1136. data,
  1137. )
  1138. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1139. `"foo<!--if-->"`,
  1140. )
  1141. data.value = false
  1142. await nextTick()
  1143. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1144. `"<!--if-->"`,
  1145. )
  1146. })
  1147. test('consecutive if node', async () => {
  1148. const data = ref(true)
  1149. const { container } = await testHydration(
  1150. `<template>
  1151. <components.Child v-if="data"/>
  1152. </template>`,
  1153. { Child: `<template><div v-if="data">foo</div></template>` },
  1154. data,
  1155. )
  1156. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1157. `"<div>foo</div><!--if--><!--if-->"`,
  1158. )
  1159. data.value = false
  1160. await nextTick()
  1161. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1162. `"<!--if-->"`,
  1163. )
  1164. data.value = true
  1165. await nextTick()
  1166. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1167. `"<div>foo</div><!--if--><!--if-->"`,
  1168. )
  1169. })
  1170. test('mixed prepend and insertion anchor', async () => {
  1171. const data = reactive({
  1172. show: true,
  1173. foo: 'foo',
  1174. bar: 'bar',
  1175. qux: 'qux',
  1176. })
  1177. const { container } = await testHydration(
  1178. `<template>
  1179. <components.Child/>
  1180. </template>`,
  1181. {
  1182. Child: `<template>
  1183. <span v-if="data.show">
  1184. <span v-if="data.show">{{data.foo}}</span>
  1185. <span v-if="data.show">{{data.bar}}</span>
  1186. <span>baz</span>
  1187. <span v-if="data.show">{{data.qux}}</span>
  1188. <span>quux</span>
  1189. </span>
  1190. </template>`,
  1191. },
  1192. data,
  1193. )
  1194. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1195. `"<span><span>foo</span><!--if--><span>bar</span><!--if--><span>baz</span><span>qux</span><!--if--><span>quux</span></span><!--if-->"`,
  1196. )
  1197. data.qux = 'qux1'
  1198. await nextTick()
  1199. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1200. `"<span><span>foo</span><!--if--><span>bar</span><!--if--><span>baz</span><span>qux1</span><!--if--><span>quux</span></span><!--if-->"`,
  1201. )
  1202. data.foo = 'foo1'
  1203. await nextTick()
  1204. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1205. `"<span><span>foo1</span><!--if--><span>bar</span><!--if--><span>baz</span><span>qux1</span><!--if--><span>quux</span></span><!--if-->"`,
  1206. )
  1207. })
  1208. test('v-if/else-if/else chain on component - switch branches', async () => {
  1209. const data = ref('a')
  1210. const { container } = await testHydration(
  1211. `<template>
  1212. <components.Child1 v-if="data === 'a'"/>
  1213. <components.Child2 v-else-if="data === 'b'"/>
  1214. <components.Child3 v-else/>
  1215. </template>`,
  1216. {
  1217. Child1: `<template><span>{{data}} child1</span></template>`,
  1218. Child2: `<template><span>{{data}} child2</span></template>`,
  1219. Child3: `<template><span>{{data}} child3</span></template>`,
  1220. },
  1221. data,
  1222. )
  1223. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1224. `"<span>a child1</span><!--if-->"`,
  1225. )
  1226. data.value = 'b'
  1227. await nextTick()
  1228. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1229. `"<span>b child2</span><!--if--><!--if-->"`,
  1230. )
  1231. data.value = 'c'
  1232. await nextTick()
  1233. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1234. `"<span>c child3</span><!--if--><!--if-->"`,
  1235. )
  1236. data.value = 'a'
  1237. await nextTick()
  1238. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1239. `"<span>a child1</span><!--if-->"`,
  1240. )
  1241. })
  1242. test('on component with insertion anchor', async () => {
  1243. const data = ref(true)
  1244. const { container } = await testHydration(
  1245. `<template>
  1246. <div>
  1247. <span/>
  1248. <components.Child v-if="data"/>
  1249. <span/>
  1250. </div>
  1251. </template>`,
  1252. { Child: `<template>foo</template>` },
  1253. data,
  1254. )
  1255. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1256. `"<div><span></span>foo<!--if--><span></span></div>"`,
  1257. )
  1258. data.value = false
  1259. await nextTick()
  1260. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1261. `"<div><span></span><!--if--><span></span></div>"`,
  1262. )
  1263. })
  1264. test('consecutive component with insertion parent', async () => {
  1265. const data = reactive({
  1266. show: true,
  1267. foo: 'foo',
  1268. bar: 'bar',
  1269. })
  1270. const { container } = await testHydration(
  1271. `<template>
  1272. <div v-if="data.show">
  1273. <components.Child/>
  1274. <components.Child2/>
  1275. </div>
  1276. </template>`,
  1277. {
  1278. Child: `<template><span>{{data.foo}}</span></template>`,
  1279. Child2: `<template><span>{{data.bar}}</span></template>`,
  1280. },
  1281. data,
  1282. )
  1283. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1284. `"<div><span>foo</span><span>bar</span></div><!--if-->"`,
  1285. )
  1286. data.show = false
  1287. await nextTick()
  1288. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1289. `"<!--if-->"`,
  1290. )
  1291. data.show = true
  1292. await nextTick()
  1293. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1294. `"<div><span>foo</span><span>bar</span></div><!--if-->"`,
  1295. )
  1296. data.foo = 'foo1'
  1297. data.bar = 'bar1'
  1298. await nextTick()
  1299. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1300. `"<div><span>foo1</span><span>bar1</span></div><!--if-->"`,
  1301. )
  1302. })
  1303. test('on fragment component', async () => {
  1304. const data = ref(true)
  1305. const { container } = await testHydration(
  1306. `<template>
  1307. <div>
  1308. <components.Child v-if="data"/>
  1309. </div>
  1310. </template>`,
  1311. {
  1312. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  1313. },
  1314. data,
  1315. )
  1316. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1317. `
  1318. "<div>
  1319. <!--[--><div>true</div>-true-<!--if--><!--]-->
  1320. </div>"
  1321. `,
  1322. )
  1323. data.value = false
  1324. await nextTick()
  1325. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1326. `
  1327. "<div>
  1328. <!--[--><!--if--><!--]-->
  1329. </div>"
  1330. `,
  1331. )
  1332. data.value = true
  1333. await nextTick()
  1334. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1335. `
  1336. "<div>
  1337. <!--[--><div>true</div>-true-<!--if--><!--]-->
  1338. </div>"
  1339. `,
  1340. )
  1341. })
  1342. test('on fragment component with insertion anchor', async () => {
  1343. const data = ref(true)
  1344. const { container } = await testHydration(
  1345. `<template>
  1346. <div>
  1347. <span/>
  1348. <components.Child v-if="data"/>
  1349. <span/>
  1350. </div>
  1351. </template>`,
  1352. {
  1353. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  1354. },
  1355. data,
  1356. )
  1357. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1358. `
  1359. "<div><span></span>
  1360. <!--[--><div>true</div>-true-<!--if--><!--]-->
  1361. <span></span></div>"
  1362. `,
  1363. )
  1364. data.value = false
  1365. await nextTick()
  1366. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1367. `
  1368. "<div><span></span>
  1369. <!--[--><!--if--><!--]-->
  1370. <span></span></div>"
  1371. `,
  1372. )
  1373. data.value = true
  1374. await nextTick()
  1375. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  1376. "<div><span></span>
  1377. <!--[--><div>true</div>-true-<!--if--><!--]-->
  1378. <span></span></div>"
  1379. `)
  1380. })
  1381. test('consecutive v-if on fragment component with insertion anchor', async () => {
  1382. const data = ref(true)
  1383. const { container } = await testHydration(
  1384. `<template>
  1385. <div>
  1386. <span/>
  1387. <components.Child v-if="data"/>
  1388. <components.Child v-if="data"/>
  1389. <span/>
  1390. </div>
  1391. </template>`,
  1392. {
  1393. Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
  1394. },
  1395. data,
  1396. )
  1397. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1398. `
  1399. "<div><span></span>
  1400. <!--[--><div>true</div>-true-<!--if--><!--]-->
  1401. <!--[--><div>true</div>-true-<!--if--><!--]-->
  1402. <span></span></div>"
  1403. `,
  1404. )
  1405. data.value = false
  1406. await nextTick()
  1407. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1408. `
  1409. "<div><span></span>
  1410. <!--[--><!--if--><!--]-->
  1411. <!--[--><!--if--><!--]-->
  1412. <span></span></div>"
  1413. `,
  1414. )
  1415. data.value = true
  1416. await nextTick()
  1417. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  1418. "<div><span></span>
  1419. <!--[--><div>true</div>-true-<!--if--><!--]-->
  1420. <!--[--><div>true</div>-true-<!--if--><!--]-->
  1421. <span></span></div>"
  1422. `)
  1423. })
  1424. test('on dynamic component with insertion anchor', async () => {
  1425. const data = ref(true)
  1426. const { container } = await testHydration(
  1427. `<template>
  1428. <div>
  1429. <span/>
  1430. <component :is="components.Child" v-if="data"/>
  1431. <span/>
  1432. </div>
  1433. </template>`,
  1434. { Child: `<template>foo</template>` },
  1435. data,
  1436. )
  1437. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1438. `"<div><span></span>foo<!--dynamic-component--><!--if--><span></span></div>"`,
  1439. )
  1440. data.value = false
  1441. await nextTick()
  1442. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1443. `"<div><span></span><!--if--><span></span></div>"`,
  1444. )
  1445. data.value = true
  1446. await nextTick()
  1447. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1448. `"<div><span></span>foo<!--dynamic-component--><!--if--><span></span></div>"`,
  1449. )
  1450. })
  1451. })
  1452. describe('for', () => {
  1453. test('basic v-for', async () => {
  1454. const { container, data } = await testHydration(
  1455. `<template>
  1456. <span v-for="item in data" :key="item">{{ item }}</span>
  1457. </template>`,
  1458. undefined,
  1459. ref(['a', 'b', 'c']),
  1460. )
  1461. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1462. `
  1463. "
  1464. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1465. "
  1466. `,
  1467. )
  1468. data.value.push('d')
  1469. await nextTick()
  1470. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1471. `
  1472. "
  1473. <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
  1474. "
  1475. `,
  1476. )
  1477. })
  1478. test('empty v-for', async () => {
  1479. const { container, data } = await testHydration(
  1480. `<template>
  1481. <span v-for="item in data" :key="item">{{ item }}</span>
  1482. </template>`,
  1483. undefined,
  1484. ref([]),
  1485. )
  1486. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1487. `
  1488. "
  1489. <!--[--><!--]-->
  1490. "
  1491. `,
  1492. )
  1493. data.value.push('a')
  1494. await nextTick()
  1495. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1496. `
  1497. "
  1498. <!--[--><span>a</span><!--]-->
  1499. "
  1500. `,
  1501. )
  1502. })
  1503. test('v-for with insertion parent + sibling component', async () => {
  1504. const { container, data } = await testHydration(
  1505. `<template>
  1506. <div>
  1507. <span v-for="item in data" :key="item">{{ item }}</span>
  1508. </div>
  1509. <components.Child/>
  1510. </template>`,
  1511. {
  1512. Child: `<template><div>{{data.length}}</div></template>`,
  1513. },
  1514. ref(['a', 'b', 'c']),
  1515. )
  1516. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1517. `
  1518. "
  1519. <!--[--><div>
  1520. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1521. </div><div>3</div><!--]-->
  1522. "
  1523. `,
  1524. )
  1525. data.value.push('d')
  1526. await nextTick()
  1527. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1528. `
  1529. "
  1530. <!--[--><div>
  1531. <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
  1532. </div><div>4</div><!--]-->
  1533. "
  1534. `,
  1535. )
  1536. })
  1537. test('v-for with insertion anchor', async () => {
  1538. const { container, data } = await testHydration(
  1539. `<template>
  1540. <div>
  1541. <span/>
  1542. <span v-for="item in data" :key="item">{{ item }}</span>
  1543. <span/>
  1544. </div>
  1545. </template>`,
  1546. undefined,
  1547. ref(['a', 'b', 'c']),
  1548. )
  1549. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1550. `
  1551. "<div><span></span>
  1552. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1553. <span></span></div>"
  1554. `,
  1555. )
  1556. data.value.push('d')
  1557. await nextTick()
  1558. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1559. `
  1560. "<div><span></span>
  1561. <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
  1562. <span></span></div>"
  1563. `,
  1564. )
  1565. data.value.splice(0, 1)
  1566. await nextTick()
  1567. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1568. `
  1569. "<div><span></span>
  1570. <!--[--><span>b</span><span>c</span><span>d</span><!--]-->
  1571. <span></span></div>"
  1572. `,
  1573. )
  1574. })
  1575. test('consecutive v-for with insertion anchor', async () => {
  1576. const { container, data } = await testHydration(
  1577. `<template>
  1578. <div>
  1579. <span/>
  1580. <span v-for="item in data" :key="item">{{ item }}</span>
  1581. <span v-for="item in data" :key="item">{{ item }}</span>
  1582. <span/>
  1583. </div>
  1584. </template>`,
  1585. undefined,
  1586. ref(['a', 'b', 'c']),
  1587. )
  1588. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1589. `
  1590. "<div><span></span>
  1591. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1592. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1593. <span></span></div>"
  1594. `,
  1595. )
  1596. data.value.push('d')
  1597. await nextTick()
  1598. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1599. `
  1600. "<div><span></span>
  1601. <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
  1602. <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
  1603. <span></span></div>"
  1604. `,
  1605. )
  1606. data.value.splice(0, 2)
  1607. await nextTick()
  1608. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1609. `
  1610. "<div><span></span>
  1611. <!--[--><span>c</span><span>d</span><!--]-->
  1612. <!--[--><span>c</span><span>d</span><!--]-->
  1613. <span></span></div>"
  1614. `,
  1615. )
  1616. })
  1617. test('v-for on component', async () => {
  1618. const { container, data } = await testHydration(
  1619. `<template>
  1620. <div>
  1621. <components.Child v-for="item in data" :key="item"/>
  1622. </div>
  1623. </template>`,
  1624. {
  1625. Child: `<template><div>comp</div></template>`,
  1626. },
  1627. ref(['a', 'b', 'c']),
  1628. )
  1629. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1630. `
  1631. "<div>
  1632. <!--[--><div>comp</div><div>comp</div><div>comp</div><!--]-->
  1633. </div>"
  1634. `,
  1635. )
  1636. data.value.push('d')
  1637. await nextTick()
  1638. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1639. `
  1640. "<div>
  1641. <!--[--><div>comp</div><div>comp</div><div>comp</div><div>comp</div><!--]-->
  1642. </div>"
  1643. `,
  1644. )
  1645. })
  1646. test('v-for on component with slots', async () => {
  1647. const { container, data } = await testHydration(
  1648. `<template>
  1649. <div>
  1650. <components.Child v-for="item in data" :key="item">
  1651. <span>{{ item }}</span>
  1652. </components.Child>
  1653. </div>
  1654. </template>`,
  1655. {
  1656. Child: `<template><slot/></template>`,
  1657. },
  1658. ref(['a', 'b', 'c']),
  1659. )
  1660. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1661. `
  1662. "<div>
  1663. <!--[-->
  1664. <!--[--><span>a</span><!--]-->
  1665. <!--[--><span>b</span><!--]-->
  1666. <!--[--><span>c</span><!--]-->
  1667. <!--]-->
  1668. </div>"
  1669. `,
  1670. )
  1671. data.value.push('d')
  1672. await nextTick()
  1673. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1674. `
  1675. "<div>
  1676. <!--[-->
  1677. <!--[--><span>a</span><!--]-->
  1678. <!--[--><span>b</span><!--]-->
  1679. <!--[--><span>c</span><!--]-->
  1680. <span>d</span><!--slot--><!--]-->
  1681. </div>"
  1682. `,
  1683. )
  1684. })
  1685. test('on fragment component', async () => {
  1686. const { container, data } = await testHydration(
  1687. `<template>
  1688. <div>
  1689. <components.Child v-for="item in data" :key="item"/>
  1690. </div>
  1691. </template>`,
  1692. {
  1693. Child: `<template><div>foo</div>-bar-</template>`,
  1694. },
  1695. ref(['a', 'b', 'c']),
  1696. )
  1697. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1698. `
  1699. "<div>
  1700. <!--[-->
  1701. <!--[--><div>foo</div>-bar-<!--]-->
  1702. <!--[--><div>foo</div>-bar-<!--]-->
  1703. <!--[--><div>foo</div>-bar-<!--]-->
  1704. <!--]-->
  1705. </div>"
  1706. `,
  1707. )
  1708. data.value.push('d')
  1709. await nextTick()
  1710. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1711. `
  1712. "<div>
  1713. <!--[-->
  1714. <!--[--><div>foo</div>-bar-<!--]-->
  1715. <!--[--><div>foo</div>-bar-<!--]-->
  1716. <!--[--><div>foo</div>-bar-<div>foo</div>-bar-<!--]-->
  1717. <!--]-->
  1718. </div>"
  1719. `,
  1720. )
  1721. })
  1722. test('on component with non-hydration node', async () => {
  1723. const data = ref({ show: true, msg: 'foo' })
  1724. const { container } = await testHydration(
  1725. `<template>
  1726. <div>
  1727. <components.Child v-for="item in 2" :key="item"/>
  1728. </div>
  1729. </template>`,
  1730. {
  1731. Child: `<template>
  1732. <div>
  1733. <div>
  1734. <div v-if="data.show">{{ data.msg }}</div>
  1735. </div>
  1736. <span>non-hydration node</span>
  1737. </div>
  1738. </template>`,
  1739. },
  1740. data,
  1741. )
  1742. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1743. `
  1744. "<div>
  1745. <!--[--><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><!--]-->
  1746. </div>"
  1747. `,
  1748. )
  1749. data.value.msg = 'bar'
  1750. await nextTick()
  1751. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1752. `
  1753. "<div>
  1754. <!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--]-->
  1755. </div>"
  1756. `,
  1757. )
  1758. data.value.show = false
  1759. await nextTick()
  1760. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  1761. "<div>
  1762. <!--[--><div><div><!--if--></div><span>non-hydration node</span></div><div><div><!--if--></div><span>non-hydration node</span></div><!--]-->
  1763. </div>"
  1764. `)
  1765. data.value.show = true
  1766. await nextTick()
  1767. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  1768. "<div>
  1769. <!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--]-->
  1770. </div>"
  1771. `)
  1772. })
  1773. })
  1774. describe('slots', () => {
  1775. test('basic slot', async () => {
  1776. const { data, container } = await testHydration(
  1777. `<template>
  1778. <components.Child>
  1779. <span>{{data}}</span>
  1780. </components.Child>
  1781. </template>`,
  1782. {
  1783. Child: `<template><slot/></template>`,
  1784. },
  1785. )
  1786. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1787. `
  1788. "
  1789. <!--[--><span>foo</span><!--]-->
  1790. "
  1791. `,
  1792. )
  1793. data.value = 'bar'
  1794. await nextTick()
  1795. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1796. `
  1797. "
  1798. <!--[--><span>bar</span><!--]-->
  1799. "
  1800. `,
  1801. )
  1802. })
  1803. test('named slot', async () => {
  1804. const { data, container } = await testHydration(
  1805. `<template>
  1806. <components.Child>
  1807. <template #foo>
  1808. <span>{{data}}</span>
  1809. </template>
  1810. </components.Child>
  1811. </template>`,
  1812. {
  1813. Child: `<template><slot/><slot name="foo"/></template>`,
  1814. },
  1815. )
  1816. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1817. `
  1818. "
  1819. <!--[-->
  1820. <!--[--><!--]-->
  1821. <!--[--><span>foo</span><!--]-->
  1822. <!--]-->
  1823. "
  1824. `,
  1825. )
  1826. data.value = 'bar'
  1827. await nextTick()
  1828. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1829. `
  1830. "
  1831. <!--[-->
  1832. <!--[--><!--]-->
  1833. <!--[--><span>bar</span><!--]-->
  1834. <!--]-->
  1835. "
  1836. `,
  1837. )
  1838. })
  1839. test('named slot with v-if', async () => {
  1840. const { data, container } = await testHydration(
  1841. `<template>
  1842. <components.Child>
  1843. <template #foo v-if="data">
  1844. <span>{{data}}</span>
  1845. </template>
  1846. </components.Child>
  1847. </template>`,
  1848. {
  1849. Child: `<template><slot name="foo"/></template>`,
  1850. },
  1851. )
  1852. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1853. `
  1854. "
  1855. <!--[--><span>foo</span><!--]-->
  1856. "
  1857. `,
  1858. )
  1859. data.value = false
  1860. await nextTick()
  1861. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1862. `
  1863. "
  1864. <!--[--><!--]-->
  1865. "
  1866. `,
  1867. )
  1868. data.value = true
  1869. await nextTick()
  1870. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  1871. "
  1872. <!--[--><span>true</span><!--]-->
  1873. "
  1874. `)
  1875. })
  1876. test('named slot with v-if and v-for', async () => {
  1877. const data = reactive({
  1878. show: true,
  1879. items: ['a', 'b', 'c'],
  1880. })
  1881. const { container } = await testHydration(
  1882. `<template>
  1883. <components.Child>
  1884. <template #foo v-if="data.show">
  1885. <span v-for="item in data.items" :key="item">{{item}}</span>
  1886. </template>
  1887. </components.Child>
  1888. </template>`,
  1889. {
  1890. Child: `<template><slot name="foo"/></template>`,
  1891. },
  1892. data,
  1893. )
  1894. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1895. `
  1896. "
  1897. <!--[-->
  1898. <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
  1899. <!--]-->
  1900. "
  1901. `,
  1902. )
  1903. data.show = false
  1904. await nextTick()
  1905. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1906. `
  1907. "
  1908. <!--[-->
  1909. <!--[--><!--]-->
  1910. "
  1911. `,
  1912. )
  1913. data.show = true
  1914. await nextTick()
  1915. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1916. `
  1917. "
  1918. <!--[-->
  1919. <!--[--><span>a</span><span>b</span><span>c</span><!--for--><!--]-->
  1920. "
  1921. `,
  1922. )
  1923. })
  1924. test('with insertion anchor', async () => {
  1925. const { data, container } = await testHydration(
  1926. `<template>
  1927. <components.Child>
  1928. <span/>
  1929. <span>{{data}}</span>
  1930. <span/>
  1931. </components.Child>
  1932. </template>`,
  1933. {
  1934. Child: `<template><slot/></template>`,
  1935. },
  1936. )
  1937. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1938. `
  1939. "
  1940. <!--[--><span></span><span>foo</span><span></span><!--]-->
  1941. "
  1942. `,
  1943. )
  1944. data.value = 'bar'
  1945. await nextTick()
  1946. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1947. `
  1948. "
  1949. <!--[--><span></span><span>bar</span><span></span><!--]-->
  1950. "
  1951. `,
  1952. )
  1953. })
  1954. test('with multi level anchor insertion', async () => {
  1955. const { data, container } = await testHydration(
  1956. `<template>
  1957. <components.Child>
  1958. <span/>
  1959. <span>{{data}}</span>
  1960. <span/>
  1961. </components.Child>
  1962. </template>`,
  1963. {
  1964. Child: `
  1965. <template>
  1966. <div/>
  1967. <div/>
  1968. <slot/>
  1969. <div/>
  1970. </div>
  1971. </template>`,
  1972. },
  1973. )
  1974. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1975. `
  1976. "
  1977. <!--[--><div></div><div></div>
  1978. <!--[--><span></span><span>foo</span><span></span><!--]-->
  1979. <div></div><!--]-->
  1980. "
  1981. `,
  1982. )
  1983. data.value = 'bar'
  1984. await nextTick()
  1985. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  1986. `
  1987. "
  1988. <!--[--><div></div><div></div>
  1989. <!--[--><span></span><span>bar</span><span></span><!--]-->
  1990. <div></div><!--]-->
  1991. "
  1992. `,
  1993. )
  1994. })
  1995. test('mixed slot and text node', async () => {
  1996. const data = reactive({
  1997. text: 'foo',
  1998. msg: 'hi',
  1999. })
  2000. const { container } = await testHydration(
  2001. `<template>
  2002. <components.Child>
  2003. <span>{{data.text}}</span>
  2004. </components.Child>
  2005. </template>`,
  2006. {
  2007. Child: `<template><div><slot/>{{data.msg}}</div></template>`,
  2008. },
  2009. data,
  2010. )
  2011. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2012. `
  2013. "<div>
  2014. <!--[--><span>foo</span><!--]-->
  2015. hi</div>"
  2016. `,
  2017. )
  2018. data.msg = 'bar'
  2019. await nextTick()
  2020. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2021. `
  2022. "<div>
  2023. <!--[--><span>foo</span><!--]-->
  2024. bar</div>"
  2025. `,
  2026. )
  2027. })
  2028. test('mixed root slot and text node', async () => {
  2029. const data = reactive({
  2030. text: 'foo',
  2031. msg: 'hi',
  2032. })
  2033. const { container } = await testHydration(
  2034. `<template>
  2035. <components.Child>
  2036. <span>{{data.text}}</span>
  2037. </components.Child>
  2038. </template>`,
  2039. {
  2040. Child: `<template>{{data.text}}<slot/>{{data.msg}}</template>`,
  2041. },
  2042. data,
  2043. )
  2044. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2045. `
  2046. "
  2047. <!--[-->foo
  2048. <!--[--><span>foo</span><!--]-->
  2049. hi<!--]-->
  2050. "
  2051. `,
  2052. )
  2053. data.msg = 'bar'
  2054. await nextTick()
  2055. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2056. `
  2057. "
  2058. <!--[-->foo
  2059. <!--[--><span>foo</span><!--]-->
  2060. bar<!--]-->
  2061. "
  2062. `,
  2063. )
  2064. })
  2065. test('mixed consecutive slot and element', async () => {
  2066. const data = reactive({
  2067. text: 'foo',
  2068. msg: 'hi',
  2069. })
  2070. const { container } = await testHydration(
  2071. `<template>
  2072. <components.Child>
  2073. <template #foo><span>{{data.text}}</span></template>
  2074. <template #bar><span>bar</span></template>
  2075. </components.Child>
  2076. </template>`,
  2077. {
  2078. Child: `<template><div><slot name="foo"/><slot name="bar"/><div>{{data.msg}}</div></div></template>`,
  2079. },
  2080. data,
  2081. )
  2082. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2083. `
  2084. "<div>
  2085. <!--[--><span>foo</span><!--]-->
  2086. <!--[--><span>bar</span><!--]-->
  2087. <div>hi</div></div>"
  2088. `,
  2089. )
  2090. data.msg = 'bar'
  2091. await nextTick()
  2092. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2093. `
  2094. "<div>
  2095. <!--[--><span>foo</span><!--]-->
  2096. <!--[--><span>bar</span><!--]-->
  2097. <div>bar</div></div>"
  2098. `,
  2099. )
  2100. })
  2101. test('mixed slot and element', async () => {
  2102. const data = reactive({
  2103. text: 'foo',
  2104. msg: 'hi',
  2105. })
  2106. const { container } = await testHydration(
  2107. `<template>
  2108. <components.Child>
  2109. <span>{{data.text}}</span>
  2110. </components.Child>
  2111. </template>`,
  2112. {
  2113. Child: `<template><div><slot/><div>{{data.msg}}</div></div></template>`,
  2114. },
  2115. data,
  2116. )
  2117. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2118. `
  2119. "<div>
  2120. <!--[--><span>foo</span><!--]-->
  2121. <div>hi</div></div>"
  2122. `,
  2123. )
  2124. data.msg = 'bar'
  2125. await nextTick()
  2126. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2127. `
  2128. "<div>
  2129. <!--[--><span>foo</span><!--]-->
  2130. <div>bar</div></div>"
  2131. `,
  2132. )
  2133. })
  2134. test('mixed slot and component', async () => {
  2135. const data = reactive({
  2136. msg1: 'foo',
  2137. msg2: 'bar',
  2138. })
  2139. const { container } = await testHydration(
  2140. `<template>
  2141. <components.Child>
  2142. <span>{{data.msg1}}</span>
  2143. </components.Child>
  2144. </template>`,
  2145. {
  2146. Child: `
  2147. <template>
  2148. <div>
  2149. <components.Child2/>
  2150. <slot/>
  2151. <components.Child2/>
  2152. </div>
  2153. </template>`,
  2154. Child2: `
  2155. <template>
  2156. <div>{{data.msg2}}</div>
  2157. </template>`,
  2158. },
  2159. data,
  2160. )
  2161. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2162. `
  2163. "<div><div>bar</div>
  2164. <!--[--><span>foo</span><!--]-->
  2165. <div>bar</div></div>"
  2166. `,
  2167. )
  2168. data.msg2 = 'hello'
  2169. await nextTick()
  2170. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2171. `
  2172. "<div><div>hello</div>
  2173. <!--[--><span>foo</span><!--]-->
  2174. <div>hello</div></div>"
  2175. `,
  2176. )
  2177. })
  2178. test('mixed slot and fragment component', async () => {
  2179. const data = reactive({
  2180. msg1: 'foo',
  2181. msg2: 'bar',
  2182. })
  2183. const { container } = await testHydration(
  2184. `<template>
  2185. <components.Child>
  2186. <span>{{data.msg1}}</span>
  2187. </components.Child>
  2188. </template>`,
  2189. {
  2190. Child: `
  2191. <template>
  2192. <div>
  2193. <components.Child2/>
  2194. <slot/>
  2195. <components.Child2/>
  2196. </div>
  2197. </template>`,
  2198. Child2: `
  2199. <template>
  2200. <div>{{data.msg1}}</div> {{data.msg2}}
  2201. </template>`,
  2202. },
  2203. data,
  2204. )
  2205. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2206. `
  2207. "<div>
  2208. <!--[--><div>foo</div> bar<!--]-->
  2209. <!--[--><span>foo</span><!--]-->
  2210. <!--[--><div>foo</div> bar<!--]-->
  2211. </div>"
  2212. `,
  2213. )
  2214. data.msg1 = 'hello'
  2215. data.msg2 = 'vapor'
  2216. await nextTick()
  2217. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2218. `
  2219. "<div>
  2220. <!--[--><div>hello</div> vapor<!--]-->
  2221. <!--[--><span>hello</span><!--]-->
  2222. <!--[--><div>hello</div> vapor<!--]-->
  2223. </div>"
  2224. `,
  2225. )
  2226. })
  2227. test('mixed slot and v-if', async () => {
  2228. const data = reactive({
  2229. show: true,
  2230. msg: 'foo',
  2231. })
  2232. const { container } = await testHydration(
  2233. `<template>
  2234. <components.Child>
  2235. <span>{{data.msg}}</span>
  2236. </components.Child>
  2237. </template>`,
  2238. {
  2239. Child: `
  2240. <template>
  2241. <div v-if="data.show">{{data.msg}}</div>
  2242. <slot/>
  2243. <div v-if="data.show">{{data.msg}}</div>
  2244. </template>`,
  2245. },
  2246. data,
  2247. )
  2248. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2249. `
  2250. "
  2251. <!--[--><div>foo</div><!--if-->
  2252. <!--[--><span>foo</span><!--]-->
  2253. <div>foo</div><!--if--><!--]-->
  2254. "
  2255. `,
  2256. )
  2257. data.show = false
  2258. await nextTick()
  2259. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2260. `
  2261. "
  2262. <!--[--><!--if-->
  2263. <!--[--><span>foo</span><!--]-->
  2264. <!--if--><!--]-->
  2265. "
  2266. `,
  2267. )
  2268. data.show = true
  2269. await nextTick()
  2270. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
  2271. "
  2272. <!--[--><div>foo</div><!--if-->
  2273. <!--[--><span>foo</span><!--]-->
  2274. <div>foo</div><!--if--><!--]-->
  2275. "
  2276. `)
  2277. })
  2278. test('mixed slot and v-for', async () => {
  2279. const data = reactive({
  2280. items: ['a', 'b', 'c'],
  2281. msg: 'foo',
  2282. })
  2283. const { container } = await testHydration(
  2284. `<template>
  2285. <components.Child>
  2286. <span>{{data.msg}}</span>
  2287. </components.Child>
  2288. </template>`,
  2289. {
  2290. Child: `
  2291. <template>
  2292. <div v-for="item in data.items" :key="item">{{item}}</div>
  2293. <slot/>
  2294. <div v-for="item in data.items" :key="item">{{item}}</div>
  2295. </template>`,
  2296. },
  2297. data,
  2298. )
  2299. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2300. `
  2301. "
  2302. <!--[-->
  2303. <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
  2304. <!--[--><span>foo</span><!--]-->
  2305. <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
  2306. <!--]-->
  2307. "
  2308. `,
  2309. )
  2310. data.items.push('d')
  2311. await nextTick()
  2312. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2313. `
  2314. "
  2315. <!--[-->
  2316. <!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
  2317. <!--[--><span>foo</span><!--]-->
  2318. <!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
  2319. <!--]-->
  2320. "
  2321. `,
  2322. )
  2323. })
  2324. test('consecutive slots', async () => {
  2325. const data = reactive({
  2326. msg1: 'foo',
  2327. msg2: 'bar',
  2328. })
  2329. const { container } = await testHydration(
  2330. `<template>
  2331. <components.Child>
  2332. <span>{{data.msg1}}</span>
  2333. <template #bar>
  2334. <span>{{data.msg2}}</span>
  2335. </template>
  2336. </components.Child>
  2337. </template>`,
  2338. {
  2339. Child: `<template><slot/><slot name="bar"/></template>`,
  2340. },
  2341. data,
  2342. )
  2343. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2344. `
  2345. "
  2346. <!--[-->
  2347. <!--[--><span>foo</span><!--]-->
  2348. <!--[--><span>bar</span><!--]-->
  2349. <!--]-->
  2350. "
  2351. `,
  2352. )
  2353. data.msg1 = 'hello'
  2354. data.msg2 = 'vapor'
  2355. await nextTick()
  2356. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2357. `
  2358. "
  2359. <!--[-->
  2360. <!--[--><span>hello</span><!--]-->
  2361. <!--[--><span>vapor</span><!--]-->
  2362. <!--]-->
  2363. "
  2364. `,
  2365. )
  2366. })
  2367. test('consecutive slots with insertion anchor', async () => {
  2368. const data = reactive({
  2369. msg1: 'foo',
  2370. msg2: 'bar',
  2371. })
  2372. const { container } = await testHydration(
  2373. `<template>
  2374. <components.Child>
  2375. <span>{{data.msg1}}</span>
  2376. <template #bar>
  2377. <span>{{data.msg2}}</span>
  2378. </template>
  2379. </components.Child>
  2380. </template>`,
  2381. {
  2382. Child: `<template>
  2383. <div>
  2384. <span/>
  2385. <slot/>
  2386. <slot name="bar"/>
  2387. <span/>
  2388. </div>
  2389. </template>`,
  2390. },
  2391. data,
  2392. )
  2393. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2394. `
  2395. "<div><span></span>
  2396. <!--[--><span>foo</span><!--]-->
  2397. <!--[--><span>bar</span><!--]-->
  2398. <span></span></div>"
  2399. `,
  2400. )
  2401. data.msg1 = 'hello'
  2402. data.msg2 = 'vapor'
  2403. await nextTick()
  2404. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2405. `
  2406. "<div><span></span>
  2407. <!--[--><span>hello</span><!--]-->
  2408. <!--[--><span>vapor</span><!--]-->
  2409. <span></span></div>"
  2410. `,
  2411. )
  2412. })
  2413. test('consecutive slots prepend', async () => {
  2414. const data = reactive({
  2415. msg1: 'foo',
  2416. msg2: 'bar',
  2417. msg3: 'baz',
  2418. })
  2419. const { container } = await testHydration(
  2420. `<template>
  2421. <components.Child>
  2422. <template #foo>
  2423. <span>{{data.msg1}}</span>
  2424. </template>
  2425. <template #bar>
  2426. <span>{{data.msg2}}</span>
  2427. </template>
  2428. </components.Child>
  2429. </template>`,
  2430. {
  2431. Child: `<template>
  2432. <div>
  2433. <slot name="foo"/>
  2434. <slot name="bar"/>
  2435. <div>{{data.msg3}}</div>
  2436. </div>
  2437. </template>`,
  2438. },
  2439. data,
  2440. )
  2441. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2442. `
  2443. "<div>
  2444. <!--[--><span>foo</span><!--]-->
  2445. <!--[--><span>bar</span><!--]-->
  2446. <div>baz</div></div>"
  2447. `,
  2448. )
  2449. data.msg1 = 'hello'
  2450. data.msg2 = 'vapor'
  2451. await nextTick()
  2452. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2453. `
  2454. "<div>
  2455. <!--[--><span>hello</span><!--]-->
  2456. <!--[--><span>vapor</span><!--]-->
  2457. <div>baz</div></div>"
  2458. `,
  2459. )
  2460. })
  2461. test('slot fallback', async () => {
  2462. const data = reactive({
  2463. foo: 'foo',
  2464. })
  2465. const { container } = await testHydration(
  2466. `<template>
  2467. <components.Child>
  2468. </components.Child>
  2469. </template>`,
  2470. {
  2471. Child: `<template><slot><span>{{data.foo}}</span></slot></template>`,
  2472. },
  2473. data,
  2474. )
  2475. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2476. `
  2477. "
  2478. <!--[--><span>foo</span><!--]-->
  2479. "
  2480. `,
  2481. )
  2482. data.foo = 'bar'
  2483. await nextTick()
  2484. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2485. `
  2486. "
  2487. <!--[--><span>bar</span><!--]-->
  2488. "
  2489. `,
  2490. )
  2491. })
  2492. test('forwarded slot', async () => {
  2493. const data = reactive({
  2494. foo: 'foo',
  2495. bar: 'bar',
  2496. })
  2497. const { container } = await testHydration(
  2498. `<template>
  2499. <div>
  2500. <components.Parent>
  2501. <span>{{data.foo}}</span>
  2502. </components.Parent>
  2503. <div>{{data.bar}}</div>
  2504. </div>
  2505. </template>`,
  2506. {
  2507. Parent: `<template><div><components.Child><slot/></components.Child></div></template>`,
  2508. Child: `<template><div><slot/></div></template>`,
  2509. },
  2510. data,
  2511. )
  2512. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2513. `
  2514. "<div><div><div>
  2515. <!--[-->
  2516. <!--[--><span>foo</span><!--]-->
  2517. <!--]-->
  2518. </div></div><div>bar</div></div>"
  2519. `,
  2520. )
  2521. data.foo = 'foo1'
  2522. data.bar = 'bar1'
  2523. await nextTick()
  2524. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2525. `
  2526. "<div><div><div>
  2527. <!--[-->
  2528. <!--[--><span>foo1</span><!--]-->
  2529. <!--]-->
  2530. </div></div><div>bar1</div></div>"
  2531. `,
  2532. )
  2533. })
  2534. test('forwarded slot with fallback', async () => {
  2535. const data = reactive({
  2536. foo: 'foo',
  2537. })
  2538. const { container } = await testHydration(
  2539. `<template>
  2540. <components.Parent/>
  2541. </template>`,
  2542. {
  2543. Parent: `<template><components.Child><slot/></components.Child></template>`,
  2544. Child: `<template><div><slot>{{data.foo}}</slot></div></template>`,
  2545. },
  2546. data,
  2547. )
  2548. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2549. `
  2550. "<div>
  2551. <!--[-->foo<!--]-->
  2552. </div>"
  2553. `,
  2554. )
  2555. data.foo = 'foo1'
  2556. await nextTick()
  2557. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2558. `
  2559. "<div>
  2560. <!--[-->foo1<!--]-->
  2561. </div>"
  2562. `,
  2563. )
  2564. })
  2565. test('forwarded slot with empty content', async () => {
  2566. const data = reactive({
  2567. foo: 'foo',
  2568. })
  2569. const { container } = await testHydration(
  2570. `<template>
  2571. <components.Foo/>
  2572. </template>`,
  2573. {
  2574. Foo: `<template>
  2575. <components.Bar>
  2576. <template #foo>
  2577. <slot name="foo" />
  2578. </template>
  2579. </components.Bar>
  2580. </template>`,
  2581. Bar: `<template>
  2582. <components.Baz>
  2583. <template #foo>
  2584. <slot name="foo" />
  2585. </template>
  2586. </components.Baz>
  2587. </template>`,
  2588. Baz: `<template>
  2589. <components.Qux>
  2590. <template #foo>
  2591. <slot name="foo" />
  2592. </template>
  2593. </components.Qux>
  2594. </template>`,
  2595. Qux: `<template>
  2596. <div>
  2597. <slot name="foo" />
  2598. <div>{{data.foo}}</div>
  2599. </div>
  2600. </template>`,
  2601. },
  2602. data,
  2603. )
  2604. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2605. `
  2606. "<div>
  2607. <!--[--><!--]-->
  2608. <div>foo</div></div>"
  2609. `,
  2610. )
  2611. data.foo = 'bar'
  2612. await nextTick()
  2613. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2614. `
  2615. "<div>
  2616. <!--[--><!--]-->
  2617. <div>bar</div></div>"
  2618. `,
  2619. )
  2620. })
  2621. })
  2622. describe('transition', async () => {
  2623. test('transition appear', async () => {
  2624. const { container } = await testHydration(
  2625. `<template>
  2626. <transition appear>
  2627. <div>foo</div>
  2628. </transition>
  2629. </template>`,
  2630. )
  2631. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2632. `"<div style="" class="v-enter-from v-enter-active">foo</div>"`,
  2633. )
  2634. expect(`mismatch`).not.toHaveBeenWarned()
  2635. })
  2636. test('transition appear work with pre-existing class', async () => {
  2637. const { container } = await testHydration(
  2638. `<template>
  2639. <transition appear>
  2640. <div class="foo">foo</div>
  2641. </transition>
  2642. </template>`,
  2643. )
  2644. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2645. `"<div class="foo v-enter-from v-enter-active" style="">foo</div>"`,
  2646. )
  2647. expect(`mismatch`).not.toHaveBeenWarned()
  2648. })
  2649. test('transition appear work with empty content', async () => {
  2650. const data = ref(true)
  2651. const { container } = await testHydration(
  2652. `<template>
  2653. <transition appear>
  2654. <slot v-if="data"></slot>
  2655. <span v-else>foo</span>
  2656. </transition>
  2657. </template>`,
  2658. undefined,
  2659. data,
  2660. )
  2661. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2662. `"<!--slot--><!--if-->"`,
  2663. )
  2664. expect(`mismatch`).not.toHaveBeenWarned()
  2665. data.value = false
  2666. await nextTick()
  2667. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2668. `"<span class="v-enter-from v-enter-active">foo</span><!--if-->"`,
  2669. )
  2670. })
  2671. test('transition appear with v-if', async () => {
  2672. const data = ref(false)
  2673. const { container } = await testHydration(
  2674. `<template>
  2675. <transition appear>
  2676. <div v-if="data">foo</div>
  2677. </transition>
  2678. </template>`,
  2679. undefined,
  2680. data,
  2681. )
  2682. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2683. `"<!--if-->"`,
  2684. )
  2685. expect(`mismatch`).not.toHaveBeenWarned()
  2686. })
  2687. test('transition appear with v-show', async () => {
  2688. const data = ref(false)
  2689. const { container } = await testHydration(
  2690. `<template>
  2691. <transition appear>
  2692. <div v-show="data">foo</div>
  2693. </transition>
  2694. </template>`,
  2695. undefined,
  2696. data,
  2697. )
  2698. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2699. `"<div style="display:none;" class="v-enter-from v-enter-active v-leave-from v-leave-active">foo</div>"`,
  2700. )
  2701. expect(`mismatch`).not.toHaveBeenWarned()
  2702. })
  2703. test('transition appear w/ event listener', async () => {
  2704. const { container } = await testHydration(
  2705. `<script setup>
  2706. import { ref } from 'vue'
  2707. const count = ref(0)
  2708. </script>
  2709. <template>
  2710. <transition appear>
  2711. <button @click="count++">{{ count }}</button>
  2712. </transition>
  2713. </template>`,
  2714. )
  2715. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2716. `"<button style="" class="v-enter-from v-enter-active">0</button>"`,
  2717. )
  2718. triggerEvent('click', container.querySelector('button')!)
  2719. await nextTick()
  2720. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  2721. `"<button style="" class="v-enter-from v-enter-active">1</button>"`,
  2722. )
  2723. })
  2724. })
  2725. describe.todo('async component', async () => {
  2726. test('async component', async () => {})
  2727. test('update async wrapper before resolve', async () => {})
  2728. test('hydrate safely when property used by async setup changed before render', async () => {})
  2729. test('unmount async wrapper before load', async () => {})
  2730. test('nested async wrapper', async () => {})
  2731. test('unmount async wrapper before load (fragment)', async () => {})
  2732. })
  2733. describe('force hydrate prop', async () => {
  2734. test('force hydrate prop with `.prop` modifier', async () => {
  2735. const { container } = await mountWithHydration(
  2736. '<input type="checkbox">',
  2737. `<input type="checkbox" .indeterminate="true"/>`,
  2738. )
  2739. expect((container.firstChild! as any).indeterminate).toBe(true)
  2740. })
  2741. test('force hydrate input v-model with non-string value bindings', async () => {
  2742. const { container } = await mountWithHydration(
  2743. '<input type="checkbox" value="true">',
  2744. `<input type="checkbox" :true-value="true"/>`,
  2745. )
  2746. expect((container.firstChild as any)._trueValue).toBe(true)
  2747. })
  2748. test.todo('force hydrate checkbox with indeterminate', async () => {
  2749. const { container } = await mountWithHydration(
  2750. '<input type="checkbox" indeterminate/>',
  2751. `<input type="checkbox" :indeterminate="true"/>`,
  2752. )
  2753. expect((container.firstChild! as any).indeterminate).toBe(true)
  2754. })
  2755. test.todo(
  2756. 'force hydrate select option with non-string value bindings',
  2757. () => {},
  2758. )
  2759. test.todo('force hydrate custom element with dynamic props', () => {})
  2760. })
  2761. describe('Teleport', () => {
  2762. test('basic', async () => {
  2763. const data = ref({
  2764. msg: ref('foo'),
  2765. disabled: ref(false),
  2766. fn: vi.fn(),
  2767. })
  2768. const teleportContainer = document.createElement('div')
  2769. teleportContainer.id = 'teleport'
  2770. teleportContainer.innerHTML =
  2771. `<!--teleport start anchor-->` +
  2772. `<span>foo</span>` +
  2773. `<span class="foo"></span>` +
  2774. `<!--teleport anchor-->`
  2775. document.body.appendChild(teleportContainer)
  2776. const { block, container } = await mountWithHydration(
  2777. '<!--teleport start--><!--teleport end-->',
  2778. `<teleport to="#teleport" :disabled="data.disabled">
  2779. <span>{{data.msg}}</span>
  2780. <span :class="data.msg" @click="data.fn"></span>
  2781. </teleport>`,
  2782. data,
  2783. )
  2784. const teleport = block as TeleportFragment
  2785. expect(teleport.anchor).toBe(container.lastChild)
  2786. expect(teleport.target).toBe(teleportContainer)
  2787. expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
  2788. expect((teleport.nodes as Node[])[0]).toBe(
  2789. teleportContainer.childNodes[1],
  2790. )
  2791. expect((teleport.nodes as Node[])[1]).toBe(
  2792. teleportContainer.childNodes[2],
  2793. )
  2794. expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[3])
  2795. expect(container.innerHTML).toMatchInlineSnapshot(
  2796. `"<!--teleport start--><!--teleport end-->"`,
  2797. )
  2798. // event handler
  2799. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  2800. expect(data.value.fn).toHaveBeenCalled()
  2801. data.value.msg = 'bar'
  2802. await nextTick()
  2803. expect(formatHtml(teleportContainer.innerHTML)).toBe(
  2804. `<!--teleport start anchor-->` +
  2805. `<span>bar</span>` +
  2806. `<span class="bar"></span>` +
  2807. `<!--teleport anchor-->`,
  2808. )
  2809. data.value.disabled = true
  2810. await nextTick()
  2811. expect(container.innerHTML).toBe(
  2812. `<!--teleport start-->` +
  2813. `<span>bar</span>` +
  2814. `<span class="bar"></span>` +
  2815. `<!--teleport end-->`,
  2816. )
  2817. expect(formatHtml(teleportContainer.innerHTML)).toMatchInlineSnapshot(
  2818. `"<!--teleport start anchor--><!--teleport anchor-->"`,
  2819. )
  2820. data.value.msg = 'baz'
  2821. await nextTick()
  2822. expect(container.innerHTML).toBe(
  2823. `<!--teleport start-->` +
  2824. `<span>baz</span>` +
  2825. `<span class="baz"></span>` +
  2826. `<!--teleport end-->`,
  2827. )
  2828. data.value.disabled = false
  2829. await nextTick()
  2830. expect(container.innerHTML).toMatchInlineSnapshot(
  2831. `"<!--teleport start--><!--teleport end-->"`,
  2832. )
  2833. expect(formatHtml(teleportContainer.innerHTML)).toBe(
  2834. `<!--teleport start anchor-->` +
  2835. `<span>baz</span>` +
  2836. `<span class="baz"></span>` +
  2837. `<!--teleport anchor-->`,
  2838. )
  2839. })
  2840. test('multiple + integration', async () => {
  2841. const data = ref({
  2842. msg: ref('foo'),
  2843. fn1: vi.fn(),
  2844. fn2: vi.fn(),
  2845. })
  2846. const code = `
  2847. <teleport to="#teleport2">
  2848. <span>{{data.msg}}</span>
  2849. <span :class="data.msg" @click="data.fn1"></span>
  2850. </teleport>
  2851. <teleport to="#teleport2">
  2852. <span>{{data.msg}}2</span>
  2853. <span :class="data.msg + 2" @click="data.fn2"></span>
  2854. </teleport>`
  2855. const SSRComp = compileVaporComponent(code, data, undefined, true)
  2856. const teleportContainer = document.createElement('div')
  2857. teleportContainer.id = 'teleport2'
  2858. const ctx = {} as any
  2859. const mainHtml = await VueServerRenderer.renderToString(
  2860. runtimeDom.createSSRApp(SSRComp),
  2861. ctx,
  2862. )
  2863. expect(mainHtml).toBe(
  2864. `<!--[-->` +
  2865. `<!--teleport start--><!--teleport end-->` +
  2866. `<!--teleport start--><!--teleport end-->` +
  2867. `<!--]-->`,
  2868. )
  2869. const teleportHtml = ctx.teleports!['#teleport2']
  2870. expect(teleportHtml).toBe(
  2871. `<!--teleport start anchor-->` +
  2872. `<span>foo</span><span class="foo"></span>` +
  2873. `<!--teleport anchor-->` +
  2874. `<!--teleport start anchor-->` +
  2875. `<span>foo2</span><span class="foo2"></span>` +
  2876. `<!--teleport anchor-->`,
  2877. )
  2878. teleportContainer.innerHTML = teleportHtml
  2879. document.body.appendChild(teleportContainer)
  2880. const { block, container } = await mountWithHydration(
  2881. mainHtml,
  2882. code,
  2883. data,
  2884. )
  2885. const teleports = block as any as TeleportFragment[]
  2886. const teleport1 = teleports[0]
  2887. const teleport2 = teleports[1]
  2888. expect(teleport1.anchor).toBe(container.childNodes[2])
  2889. expect(teleport2.anchor).toBe(container.childNodes[4])
  2890. expect(teleport1.target).toBe(teleportContainer)
  2891. expect(teleport1.targetStart).toBe(teleportContainer.childNodes[0])
  2892. expect((teleport1.nodes as Node[])[0]).toBe(
  2893. teleportContainer.childNodes[1],
  2894. )
  2895. expect(teleport1.targetAnchor).toBe(teleportContainer.childNodes[3])
  2896. expect(teleport2.target).toBe(teleportContainer)
  2897. expect(teleport2.targetStart).toBe(teleportContainer.childNodes[4])
  2898. expect((teleport2.nodes as Node[])[0]).toBe(
  2899. teleportContainer.childNodes[5],
  2900. )
  2901. expect(teleport2.targetAnchor).toBe(teleportContainer.childNodes[7])
  2902. expect(container.innerHTML).toBe(
  2903. `<!--[-->` +
  2904. `<!--teleport start--><!--teleport end-->` +
  2905. `<!--teleport start--><!--teleport end-->` +
  2906. `<!--]-->`,
  2907. )
  2908. // event handler
  2909. triggerEvent('click', teleportContainer.querySelector('.foo')!)
  2910. expect(data.value.fn1).toHaveBeenCalled()
  2911. triggerEvent('click', teleportContainer.querySelector('.foo2')!)
  2912. expect(data.value.fn2).toHaveBeenCalled()
  2913. data.value.msg = 'bar'
  2914. await nextTick()
  2915. expect(teleportContainer.innerHTML).toBe(
  2916. `<!--teleport start anchor-->` +
  2917. `<span>bar</span>` +
  2918. `<span class="bar"></span>` +
  2919. `<!--teleport anchor-->` +
  2920. `<!--teleport start anchor-->` +
  2921. `<span>bar2</span>` +
  2922. `<span class="bar2"></span>` +
  2923. `<!--teleport anchor-->`,
  2924. )
  2925. })
  2926. test('disabled', async () => {
  2927. const data = ref({
  2928. msg: ref('foo'),
  2929. fn1: vi.fn(),
  2930. fn2: vi.fn(),
  2931. })
  2932. const code = `
  2933. <div>foo</div>
  2934. <teleport to="#teleport3" disabled="true">
  2935. <span>{{data.msg}}</span>
  2936. <span :class="data.msg" @click="data.fn1"></span>
  2937. </teleport>
  2938. <div :class="data.msg + 2" @click="data.fn2">bar</div>
  2939. `
  2940. const SSRComp = compileVaporComponent(code, data, undefined, true)
  2941. const teleportContainer = document.createElement('div')
  2942. teleportContainer.id = 'teleport3'
  2943. const ctx = {} as any
  2944. const mainHtml = await VueServerRenderer.renderToString(
  2945. runtimeDom.createSSRApp(SSRComp),
  2946. ctx,
  2947. )
  2948. expect(mainHtml).toBe(
  2949. `<!--[-->` +
  2950. `<div>foo</div>` +
  2951. `<!--teleport start-->` +
  2952. `<span>foo</span>` +
  2953. `<span class="foo"></span>` +
  2954. `<!--teleport end-->` +
  2955. `<div class="foo2">bar</div>` +
  2956. `<!--]-->`,
  2957. )
  2958. const teleportHtml = ctx.teleports!['#teleport3']
  2959. expect(teleportHtml).toMatchInlineSnapshot(
  2960. `"<!--teleport start anchor--><!--teleport anchor-->"`,
  2961. )
  2962. teleportContainer.innerHTML = teleportHtml
  2963. document.body.appendChild(teleportContainer)
  2964. const { block, container } = await mountWithHydration(
  2965. mainHtml,
  2966. code,
  2967. data,
  2968. )
  2969. const blocks = block as any[]
  2970. expect(blocks[0]).toBe(container.childNodes[1])
  2971. const teleport = blocks[1] as TeleportFragment
  2972. expect((teleport.nodes as Node[])[0]).toBe(container.childNodes[3])
  2973. expect((teleport.nodes as Node[])[1]).toBe(container.childNodes[4])
  2974. expect(teleport.anchor).toBe(container.childNodes[5])
  2975. expect(teleport.target).toBe(teleportContainer)
  2976. expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
  2977. expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[1])
  2978. expect(blocks[2]).toBe(container.childNodes[6])
  2979. expect(container.innerHTML).toBe(
  2980. `<!--[-->` +
  2981. `<div>foo</div>` +
  2982. `<!--teleport start-->` +
  2983. `<span>foo</span>` +
  2984. `<span class="foo"></span>` +
  2985. `<!--teleport end-->` +
  2986. `<div class="foo2">bar</div>` +
  2987. `<!--]-->`,
  2988. )
  2989. // event handler
  2990. triggerEvent('click', container.querySelector('.foo')!)
  2991. expect(data.value.fn1).toHaveBeenCalled()
  2992. triggerEvent('click', container.querySelector('.foo2')!)
  2993. expect(data.value.fn2).toHaveBeenCalled()
  2994. data.value.msg = 'bar'
  2995. await nextTick()
  2996. expect(container.innerHTML).toBe(
  2997. `<!--[-->` +
  2998. `<div>foo</div>` +
  2999. `<!--teleport start-->` +
  3000. `<span>bar</span>` +
  3001. `<span class="bar"></span>` +
  3002. `<!--teleport end-->` +
  3003. `<div class="bar2">bar</div>` +
  3004. `<!--]-->`,
  3005. )
  3006. })
  3007. test('disabled + as component root', async () => {
  3008. const { container } = await mountWithHydration(
  3009. `<!--[-->` +
  3010. `<div>Parent fragment</div>` +
  3011. `<!--teleport start--><div>Teleport content</div><!--teleport end-->` +
  3012. `<!--]-->`,
  3013. `
  3014. <div>Parent fragment</div>
  3015. <teleport to="body" disabled>
  3016. <div>Teleport content</div>
  3017. </teleport>
  3018. `,
  3019. )
  3020. expect(container.innerHTML).toBe(
  3021. `<!--[-->` +
  3022. `<div>Parent fragment</div>` +
  3023. `<!--teleport start-->` +
  3024. `<div>Teleport content</div>` +
  3025. `<!--teleport end-->` +
  3026. `<!--]-->`,
  3027. )
  3028. expect(`mismatch`).not.toHaveBeenWarned()
  3029. })
  3030. test('as component root', async () => {
  3031. const teleportContainer = document.createElement('div')
  3032. teleportContainer.id = 'teleport4'
  3033. teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
  3034. document.body.appendChild(teleportContainer)
  3035. const { block, container } = await mountWithHydration(
  3036. '<!--teleport start--><!--teleport end-->',
  3037. `<components.Wrapper></components.Wrapper>`,
  3038. undefined,
  3039. {
  3040. Wrapper: compileVaporComponent(
  3041. `<teleport to="#teleport4">hello</teleport>`,
  3042. ),
  3043. },
  3044. )
  3045. const teleport = (block as VaporComponentInstance)
  3046. .block as TeleportFragment
  3047. expect(teleport.anchor).toBe(container.childNodes[1])
  3048. expect(teleport.target).toBe(teleportContainer)
  3049. expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
  3050. expect(teleport.nodes).toBe(teleportContainer.childNodes[1])
  3051. expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[2])
  3052. })
  3053. test('nested', async () => {
  3054. const teleportContainer = document.createElement('div')
  3055. teleportContainer.id = 'teleport5'
  3056. teleportContainer.innerHTML =
  3057. `<!--teleport start anchor-->` +
  3058. `<!--teleport start--><!--teleport end-->` +
  3059. `<!--teleport anchor-->` +
  3060. `<!--teleport start anchor-->` +
  3061. `<div>child</div>` +
  3062. `<!--teleport anchor-->`
  3063. document.body.appendChild(teleportContainer)
  3064. const { block, container } = await mountWithHydration(
  3065. '<!--teleport start--><!--teleport end-->',
  3066. `<teleport to="#teleport5">
  3067. <teleport to="#teleport5"><div>child</div></teleport>
  3068. </teleport>`,
  3069. )
  3070. const teleport = block as TeleportFragment
  3071. expect(teleport.anchor).toBe(container.childNodes[1])
  3072. expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
  3073. expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[3])
  3074. const childTeleport = teleport.nodes as TeleportFragment
  3075. expect(childTeleport.anchor).toBe(teleportContainer.childNodes[2])
  3076. expect(childTeleport.targetStart).toBe(teleportContainer.childNodes[4])
  3077. expect(childTeleport.targetAnchor).toBe(teleportContainer.childNodes[6])
  3078. expect(childTeleport.nodes).toBe(teleportContainer.childNodes[5])
  3079. })
  3080. test('unmount (full integration)', async () => {
  3081. const targetId = 'teleport6'
  3082. const data = ref({
  3083. toggle: ref(true),
  3084. })
  3085. const template1 = `<Teleport to="#${targetId}"><span>Teleported Comp1</span></Teleport>`
  3086. const Comp1 = compileVaporComponent(template1)
  3087. const SSRComp1 = compileVaporComponent(
  3088. template1,
  3089. undefined,
  3090. undefined,
  3091. true,
  3092. )
  3093. const template2 = `<div>Comp2</div>`
  3094. const Comp2 = compileVaporComponent(template2)
  3095. const SSRComp2 = compileVaporComponent(
  3096. template2,
  3097. undefined,
  3098. undefined,
  3099. true,
  3100. )
  3101. const appCode = `
  3102. <div>
  3103. <components.Comp1 v-if="data.toggle"/>
  3104. <components.Comp2 v-else/>
  3105. </div>
  3106. `
  3107. const SSRApp = compileVaporComponent(
  3108. appCode,
  3109. data,
  3110. {
  3111. Comp1: SSRComp1,
  3112. Comp2: SSRComp2,
  3113. },
  3114. true,
  3115. )
  3116. const teleportContainer = document.createElement('div')
  3117. teleportContainer.id = targetId
  3118. document.body.appendChild(teleportContainer)
  3119. const ctx = {} as any
  3120. const mainHtml = await VueServerRenderer.renderToString(
  3121. runtimeDom.createSSRApp(SSRApp),
  3122. ctx,
  3123. )
  3124. expect(mainHtml).toBe(
  3125. '<div><!--teleport start--><!--teleport end--></div>',
  3126. )
  3127. teleportContainer.innerHTML = ctx.teleports![`#${targetId}`]
  3128. const { container } = await mountWithHydration(mainHtml, appCode, data, {
  3129. Comp1,
  3130. Comp2,
  3131. })
  3132. expect(container.innerHTML).toBe(
  3133. '<div><!--teleport start--><!--teleport end--><!--if--></div>',
  3134. )
  3135. expect(teleportContainer.innerHTML).toBe(
  3136. `<!--teleport start anchor-->` +
  3137. `<span>Teleported Comp1</span>` +
  3138. `<!--teleport anchor-->`,
  3139. )
  3140. expect(`mismatch`).not.toHaveBeenWarned()
  3141. data.value.toggle = false
  3142. await nextTick()
  3143. expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
  3144. expect(teleportContainer.innerHTML).toBe('')
  3145. })
  3146. test('unmount (mismatch + full integration)', async () => {
  3147. const targetId = 'teleport7'
  3148. const data = ref({
  3149. toggle: ref(true),
  3150. })
  3151. const template1 = `<Teleport to="#${targetId}"><span>Teleported Comp1</span></Teleport>`
  3152. const Comp1 = compileVaporComponent(template1)
  3153. const SSRComp1 = compileVaporComponent(
  3154. template1,
  3155. undefined,
  3156. undefined,
  3157. true,
  3158. )
  3159. const template2 = `<div>Comp2</div>`
  3160. const Comp2 = compileVaporComponent(template2)
  3161. const SSRComp2 = compileVaporComponent(
  3162. template2,
  3163. undefined,
  3164. undefined,
  3165. true,
  3166. )
  3167. const appCode = `
  3168. <div>
  3169. <components.Comp1 v-if="data.toggle"/>
  3170. <components.Comp2 v-else/>
  3171. </div>
  3172. `
  3173. const SSRApp = compileVaporComponent(
  3174. appCode,
  3175. data,
  3176. {
  3177. Comp1: SSRComp1,
  3178. Comp2: SSRComp2,
  3179. },
  3180. true,
  3181. )
  3182. const teleportContainer = document.createElement('div')
  3183. teleportContainer.id = targetId
  3184. document.body.appendChild(teleportContainer)
  3185. const mainHtml = await VueServerRenderer.renderToString(
  3186. runtimeDom.createSSRApp(SSRApp),
  3187. )
  3188. expect(mainHtml).toBe(
  3189. '<div><!--teleport start--><!--teleport end--></div>',
  3190. )
  3191. expect(teleportContainer.innerHTML).toBe('')
  3192. const { container } = await mountWithHydration(mainHtml, appCode, data, {
  3193. Comp1,
  3194. Comp2,
  3195. })
  3196. expect(container.innerHTML).toBe(
  3197. '<div><!--teleport start--><!--teleport end--><!--if--></div>',
  3198. )
  3199. expect(teleportContainer.innerHTML).toBe(`<span>Teleported Comp1</span>`)
  3200. expect(`Hydration children mismatch`).toHaveBeenWarned()
  3201. data.value.toggle = false
  3202. await nextTick()
  3203. expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
  3204. expect(teleportContainer.innerHTML).toBe('')
  3205. })
  3206. test('target change (mismatch + full integration)', async () => {
  3207. const targetId1 = 'teleport8-1'
  3208. const targetId2 = 'teleport8-2'
  3209. const data = ref({
  3210. target: ref(targetId1),
  3211. msg: ref('foo'),
  3212. })
  3213. const template = `<Teleport :to="'#' + data.target"><span>{{data.msg}}</span></Teleport>`
  3214. const Comp = compileVaporComponent(template, data)
  3215. const SSRComp = compileVaporComponent(template, data, undefined, true)
  3216. const teleportContainer1 = document.createElement('div')
  3217. teleportContainer1.id = targetId1
  3218. const teleportContainer2 = document.createElement('div')
  3219. teleportContainer2.id = targetId2
  3220. document.body.appendChild(teleportContainer1)
  3221. document.body.appendChild(teleportContainer2)
  3222. // server render
  3223. const mainHtml = await VueServerRenderer.renderToString(
  3224. runtimeDom.createSSRApp(SSRComp),
  3225. )
  3226. expect(mainHtml).toBe(`<!--teleport start--><!--teleport end-->`)
  3227. expect(teleportContainer1.innerHTML).toBe('')
  3228. expect(teleportContainer2.innerHTML).toBe('')
  3229. // hydrate
  3230. const { container } = await mountWithHydration(mainHtml, template, data, {
  3231. Comp,
  3232. })
  3233. expect(container.innerHTML).toBe(
  3234. `<!--teleport start--><!--teleport end-->`,
  3235. )
  3236. expect(teleportContainer1.innerHTML).toBe(`<span>foo</span>`)
  3237. expect(teleportContainer2.innerHTML).toBe('')
  3238. expect(`Hydration children mismatch`).toHaveBeenWarned()
  3239. data.value.target = targetId2
  3240. data.value.msg = 'bar'
  3241. await nextTick()
  3242. expect(container.innerHTML).toBe(
  3243. `<!--teleport start--><!--teleport end-->`,
  3244. )
  3245. expect(teleportContainer1.innerHTML).toBe('')
  3246. expect(teleportContainer2.innerHTML).toBe(`<span>bar</span>`)
  3247. })
  3248. test('with disabled teleport + undefined target', async () => {
  3249. const data = ref({
  3250. msg: ref('foo'),
  3251. })
  3252. const { container } = await mountWithHydration(
  3253. '<!--teleport start--><span>foo</span><!--teleport end-->',
  3254. `<teleport :to="undefined" :disabled="true">
  3255. <span>{{data.msg}}</span>
  3256. </teleport>`,
  3257. data,
  3258. )
  3259. expect(container.innerHTML).toBe(
  3260. `<!--teleport start--><span>foo</span><!--teleport end-->`,
  3261. )
  3262. data.value.msg = 'bar'
  3263. await nextTick()
  3264. expect(container.innerHTML).toBe(
  3265. `<!--teleport start--><span>bar</span><!--teleport end-->`,
  3266. )
  3267. })
  3268. })
  3269. describe.todo('Suspense')
  3270. })
  3271. describe('mismatch handling', () => {
  3272. test('text node', async () => {
  3273. const foo = ref('bar')
  3274. const { container } = await mountWithHydration(`foo`, `{{data}}`, foo)
  3275. expect(container.textContent).toBe('bar')
  3276. expect(`Hydration text mismatch`).toHaveBeenWarned()
  3277. })
  3278. test('element text content', async () => {
  3279. const data = ref({ textContent: 'bar' })
  3280. const { container } = await mountWithHydration(
  3281. `<div>foo</div>`,
  3282. `<div v-bind="data"></div>`,
  3283. data,
  3284. )
  3285. expect(container.innerHTML).toBe('<div>bar</div>')
  3286. expect(`Hydration text content mismatch`).toHaveBeenWarned()
  3287. })
  3288. // test('not enough children', () => {
  3289. // const { container } = mountWithHydration(`<div></div>`, () =>
  3290. // h('div', [h('span', 'foo'), h('span', 'bar')]),
  3291. // )
  3292. // expect(container.innerHTML).toBe(
  3293. // '<div><span>foo</span><span>bar</span></div>',
  3294. // )
  3295. // expect(`Hydration children mismatch`).toHaveBeenWarned()
  3296. // })
  3297. // test('too many children', () => {
  3298. // const { container } = mountWithHydration(
  3299. // `<div><span>foo</span><span>bar</span></div>`,
  3300. // () => h('div', [h('span', 'foo')]),
  3301. // )
  3302. // expect(container.innerHTML).toBe('<div><span>foo</span></div>')
  3303. // expect(`Hydration children mismatch`).toHaveBeenWarned()
  3304. // })
  3305. test('complete mismatch', async () => {
  3306. const data = ref('span')
  3307. const { container } = await mountWithHydration(
  3308. `<div>foo</div>`,
  3309. `<component :is="data">foo</component>`,
  3310. data,
  3311. )
  3312. expect(container.innerHTML).toBe('<span>foo</span><!--dynamic-component-->')
  3313. expect(`Hydration node mismatch`).toHaveBeenWarned()
  3314. })
  3315. // test('fragment mismatch removal', () => {
  3316. // const { container } = mountWithHydration(
  3317. // `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  3318. // () => h('div', [h('span', 'replaced')]),
  3319. // )
  3320. // expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
  3321. // expect(`Hydration node mismatch`).toHaveBeenWarned()
  3322. // })
  3323. // test('fragment not enough children', () => {
  3324. // const { container } = mountWithHydration(
  3325. // `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  3326. // () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
  3327. // )
  3328. // expect(container.innerHTML).toBe(
  3329. // '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
  3330. // )
  3331. // expect(`Hydration node mismatch`).toHaveBeenWarned()
  3332. // })
  3333. // test('fragment too many children', () => {
  3334. // const { container } = mountWithHydration(
  3335. // `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  3336. // () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
  3337. // )
  3338. // expect(container.innerHTML).toBe(
  3339. // '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>',
  3340. // )
  3341. // // fragment ends early and attempts to hydrate the extra <div>bar</div>
  3342. // // as 2nd fragment child.
  3343. // expect(`Hydration text content mismatch`).toHaveBeenWarned()
  3344. // // excessive children removal
  3345. // expect(`Hydration children mismatch`).toHaveBeenWarned()
  3346. // })
  3347. // test('Teleport target has empty children', () => {
  3348. // const teleportContainer = document.createElement('div')
  3349. // teleportContainer.id = 'teleport'
  3350. // document.body.appendChild(teleportContainer)
  3351. // mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
  3352. // h(Teleport, { to: '#teleport' }, [h('span', 'value')]),
  3353. // )
  3354. // expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
  3355. // expect(`Hydration children mismatch`).toHaveBeenWarned()
  3356. // })
  3357. // test('comment mismatch (element)', () => {
  3358. // const { container } = mountWithHydration(`<div><span></span></div>`, () =>
  3359. // h('div', [createCommentVNode('hi')]),
  3360. // )
  3361. // expect(container.innerHTML).toBe('<div><!--hi--></div>')
  3362. // expect(`Hydration node mismatch`).toHaveBeenWarned()
  3363. // })
  3364. // test('comment mismatch (text)', () => {
  3365. // const { container } = mountWithHydration(`<div>foobar</div>`, () =>
  3366. // h('div', [createCommentVNode('hi')]),
  3367. // )
  3368. // expect(container.innerHTML).toBe('<div><!--hi--></div>')
  3369. // expect(`Hydration node mismatch`).toHaveBeenWarned()
  3370. // })
  3371. test('class mismatch', async () => {
  3372. await mountWithHydration(
  3373. `<div class="foo bar"></div>`,
  3374. `<div :class="data"></div>`,
  3375. ref(['foo', 'bar']),
  3376. )
  3377. await mountWithHydration(
  3378. `<div class="foo bar"></div>`,
  3379. `<div :class="data"></div>`,
  3380. ref({ foo: true, bar: true }),
  3381. )
  3382. await mountWithHydration(
  3383. `<div class="foo bar"></div>`,
  3384. `<div :class="data"></div>`,
  3385. ref('foo bar'),
  3386. )
  3387. // svg classes
  3388. await mountWithHydration(
  3389. `<svg class="foo bar"></svg>`,
  3390. `<svg :class="data"></svg>`,
  3391. ref('foo bar'),
  3392. )
  3393. // class with different order
  3394. await mountWithHydration(
  3395. `<div class="foo bar"></div>`,
  3396. `<div :class="data"></div>`,
  3397. ref('bar foo'),
  3398. )
  3399. expect(`Hydration class mismatch`).not.toHaveBeenWarned()
  3400. // single root mismatch
  3401. const { container: root } = await mountWithHydration(
  3402. `<div class="foo bar"></div>`,
  3403. `<div :class="data"></div>`,
  3404. ref('baz'),
  3405. )
  3406. expect(root.innerHTML).toBe('<div class="foo bar baz"></div>')
  3407. expect(`Hydration class mismatch`).toHaveBeenWarned()
  3408. // multiple root mismatch
  3409. const { container } = await mountWithHydration(
  3410. `<div class="foo bar"></div><span/>`,
  3411. `<div :class="data"></div><span/>`,
  3412. ref('foo'),
  3413. )
  3414. expect(container.innerHTML).toBe('<div class="foo"></div><span></span>')
  3415. expect(`Hydration class mismatch`).toHaveBeenWarned()
  3416. })
  3417. test('style mismatch', async () => {
  3418. await mountWithHydration(
  3419. `<div style="color:red;"></div>`,
  3420. `<div :style="data"></div>`,
  3421. ref({ color: 'red' }),
  3422. )
  3423. await mountWithHydration(
  3424. `<div style="color:red;"></div>`,
  3425. `<div :style="data"></div>`,
  3426. ref('color:red;'),
  3427. )
  3428. // style with different order
  3429. await mountWithHydration(
  3430. `<div style="color:red; font-size: 12px;"></div>`,
  3431. `<div :style="data"></div>`,
  3432. ref(`font-size: 12px; color:red;`),
  3433. )
  3434. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  3435. // single root mismatch
  3436. const { container: root } = await mountWithHydration(
  3437. `<div style="color:red;"></div>`,
  3438. `<div :style="data"></div>`,
  3439. ref({ color: 'green' }),
  3440. )
  3441. expect(root.innerHTML).toBe('<div style="color: green;"></div>')
  3442. expect(`Hydration style mismatch`).toHaveBeenWarned()
  3443. // multiple root mismatch
  3444. const { container } = await mountWithHydration(
  3445. `<div style="color:red;"></div><span/>`,
  3446. `<div :style="data"></div><span/>`,
  3447. ref({ color: 'green' }),
  3448. )
  3449. expect(container.innerHTML).toBe(
  3450. '<div style="color: green;"></div><span></span>',
  3451. )
  3452. expect(`Hydration style mismatch`).toHaveBeenWarned()
  3453. })
  3454. test('style mismatch when no style attribute is present', async () => {
  3455. await mountWithHydration(
  3456. `<div></div>`,
  3457. `<div :style="data"></div>`,
  3458. ref({ color: 'red' }),
  3459. )
  3460. expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
  3461. })
  3462. test('style mismatch w/ v-show', async () => {
  3463. await mountWithHydration(
  3464. `<div style="color:red;display:none"></div>`,
  3465. `<div v-show="data" style="color: red;"></div>`,
  3466. ref(false),
  3467. )
  3468. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  3469. // mismatch with single root
  3470. const { container: root } = await mountWithHydration(
  3471. `<div style="color:red;"></div>`,
  3472. `<div v-show="data" style="color: red;"></div>`,
  3473. ref(false),
  3474. )
  3475. expect(root.innerHTML).toBe(
  3476. '<div style="color: red; display: none;"></div>',
  3477. )
  3478. expect(`Hydration style mismatch`).toHaveBeenWarned()
  3479. // mismatch with multiple root
  3480. const { container } = await mountWithHydration(
  3481. `<div style="color:red;"></div><span/>`,
  3482. `<div v-show="data.show" :style="data.style"></div><span/>`,
  3483. ref({ show: false, style: 'color: red' }),
  3484. )
  3485. expect(container.innerHTML).toBe(
  3486. '<div style="color: red; display: none;"></div><span></span>',
  3487. )
  3488. expect(`Hydration style mismatch`).toHaveBeenWarned()
  3489. })
  3490. test('attr mismatch', async () => {
  3491. await mountWithHydration(
  3492. `<div id="foo"></div>`,
  3493. `<div :id="data"></div>`,
  3494. ref('foo'),
  3495. )
  3496. await mountWithHydration(
  3497. `<div spellcheck></div>`,
  3498. `<div :spellcheck="data"></div>`,
  3499. ref(''),
  3500. )
  3501. await mountWithHydration(
  3502. `<div></div>`,
  3503. `<div :id="data"></div>`,
  3504. ref(undefined),
  3505. )
  3506. // boolean
  3507. await mountWithHydration(
  3508. `<select multiple></div>`,
  3509. `<select :multiple="data"></select>`,
  3510. ref(true),
  3511. )
  3512. await mountWithHydration(
  3513. `<select multiple></div>`,
  3514. `<select :multiple="data"></select>`,
  3515. ref('multiple'),
  3516. )
  3517. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  3518. await mountWithHydration(
  3519. `<div></div>`,
  3520. `<div :id="data"></div>`,
  3521. ref('foo'),
  3522. )
  3523. expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(1)
  3524. await mountWithHydration(
  3525. `<div id="bar"></div>`,
  3526. `<div :id="data"></div>`,
  3527. ref('foo'),
  3528. )
  3529. expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2)
  3530. })
  3531. test('attr special case: textarea value', async () => {
  3532. await mountWithHydration(
  3533. `<textarea>foo</textarea>`,
  3534. `<textarea :value="data"></textarea>`,
  3535. ref('foo'),
  3536. )
  3537. await mountWithHydration(
  3538. `<textarea></textarea>`,
  3539. `<textarea :value="data"></textarea>`,
  3540. ref(''),
  3541. )
  3542. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  3543. await mountWithHydration(
  3544. `<textarea>foo</textarea>`,
  3545. `<textarea :value="data"></textarea>`,
  3546. ref('bar'),
  3547. )
  3548. expect(`Hydration attribute mismatch`).toHaveBeenWarned()
  3549. })
  3550. test('<textarea> with newlines at the beginning', async () => {
  3551. await mountWithHydration(
  3552. `<textarea>\nhello</textarea>`,
  3553. `<textarea :value="data"></textarea>`,
  3554. ref('\nhello'),
  3555. )
  3556. await mountWithHydration(
  3557. `<textarea>\nhello</textarea>`,
  3558. `<textarea v-text="data"></textarea>`,
  3559. ref('\nhello'),
  3560. )
  3561. await mountWithHydration(
  3562. `<textarea>\nhello</textarea>`,
  3563. `<textarea v-bind="data"></textarea>`,
  3564. ref({ textContent: '\nhello' }),
  3565. )
  3566. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  3567. })
  3568. test('<pre> with newlines at the beginning', async () => {
  3569. await mountWithHydration(`<pre>\n</pre>`, `<pre>{{data}}</pre>`, ref('\n'))
  3570. await mountWithHydration(
  3571. `<pre>\n</pre>`,
  3572. `<pre v-text="data"></pre>`,
  3573. ref('\n'),
  3574. )
  3575. await mountWithHydration(
  3576. `<pre>\n</pre>`,
  3577. `<pre v-bind="data"></pre>`,
  3578. ref({ textContent: '\n' }),
  3579. )
  3580. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  3581. })
  3582. test('boolean attr handling', async () => {
  3583. await mountWithHydration(
  3584. `<input />`,
  3585. `<input :readonly="data" />`,
  3586. ref(false),
  3587. )
  3588. await mountWithHydration(
  3589. `<input readonly />`,
  3590. `<input :readonly="data" />`,
  3591. ref(true),
  3592. )
  3593. await mountWithHydration(
  3594. `<input readonly="readonly" />`,
  3595. `<input :readonly="data" />`,
  3596. ref(true),
  3597. )
  3598. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  3599. })
  3600. test('client value is null or undefined', async () => {
  3601. await mountWithHydration(
  3602. `<div></div>`,
  3603. `<div :draggable="data"></div>`,
  3604. ref(undefined),
  3605. )
  3606. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  3607. await mountWithHydration(`<input />`, `<input :type="data" />`, ref(null))
  3608. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  3609. })
  3610. test('should not warn against object values', async () => {
  3611. await mountWithHydration(`<input />`, `<input :from="data" />`, ref({}))
  3612. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  3613. })
  3614. test('should not warn on falsy bindings of non-property keys', async () => {
  3615. await mountWithHydration(
  3616. `<button></button>`,
  3617. `<button :href="data"></button>`,
  3618. ref(undefined),
  3619. )
  3620. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  3621. })
  3622. test('should not warn on non-renderable option values', async () => {
  3623. await mountWithHydration(
  3624. `<select><option>hello</option></select>`,
  3625. `<select><option :value="data">hello</option></select>`,
  3626. ref(['foo']),
  3627. )
  3628. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  3629. })
  3630. test.todo('should not warn css v-bind', () => {
  3631. // const container = document.createElement('div')
  3632. // container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
  3633. // const app = createSSRApp({
  3634. // setup() {
  3635. // useCssVars(() => ({
  3636. // foo: 'red',
  3637. // }))
  3638. // return () => h('div', { style: { color: 'var(--foo)' } })
  3639. // },
  3640. // })
  3641. // app.mount(container)
  3642. // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  3643. })
  3644. test.todo(
  3645. 'css vars should only be added to expected on component root dom',
  3646. () => {
  3647. // const container = document.createElement('div')
  3648. // container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
  3649. // const app = createSSRApp({
  3650. // setup() {
  3651. // useCssVars(() => ({
  3652. // foo: 'red',
  3653. // }))
  3654. // return () =>
  3655. // h('div', null, [h('div', { style: { color: 'var(--foo)' } })])
  3656. // },
  3657. // })
  3658. // app.mount(container)
  3659. // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  3660. },
  3661. )
  3662. test.todo('css vars support fallthrough', () => {
  3663. // const container = document.createElement('div')
  3664. // container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
  3665. // const app = createSSRApp({
  3666. // setup() {
  3667. // useCssVars(() => ({
  3668. // foo: 'red',
  3669. // }))
  3670. // return () => h(Child)
  3671. // },
  3672. // })
  3673. // const Child = {
  3674. // setup() {
  3675. // return () => h('div', { style: 'padding: 4px' })
  3676. // },
  3677. // }
  3678. // app.mount(container)
  3679. // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  3680. })
  3681. // vapor directive does not have a created hook
  3682. test('should not warn for directives that mutate DOM in created', () => {
  3683. // const container = document.createElement('div')
  3684. // container.innerHTML = `<div class="test red"></div>`
  3685. // const vColor: ObjectDirective = {
  3686. // created(el, binding) {
  3687. // el.classList.add(binding.value)
  3688. // },
  3689. // }
  3690. // const app = createSSRApp({
  3691. // setup() {
  3692. // return () =>
  3693. // withDirectives(h('div', { class: 'test' }), [[vColor, 'red']])
  3694. // },
  3695. // })
  3696. // app.mount(container)
  3697. // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  3698. })
  3699. test.todo('escape css var name', () => {
  3700. // const container = document.createElement('div')
  3701. // container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`
  3702. // const app = createSSRApp({
  3703. // setup() {
  3704. // useCssVars(() => ({
  3705. // 'foo.bar': 'red',
  3706. // }))
  3707. // return () => h(Child)
  3708. // },
  3709. // })
  3710. // const Child = {
  3711. // setup() {
  3712. // return () => h('div', { style: 'padding: 4px' })
  3713. // },
  3714. // }
  3715. // app.mount(container)
  3716. // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  3717. })
  3718. })
  3719. describe('data-allow-mismatch', () => {
  3720. test('element text content', async () => {
  3721. const data = ref({ textContent: 'bar' })
  3722. const { container } = await mountWithHydration(
  3723. `<div data-allow-mismatch="text">foo</div>`,
  3724. `<div v-bind="data"></div>`,
  3725. data,
  3726. )
  3727. expect(container.innerHTML).toBe(
  3728. '<div data-allow-mismatch="text">bar</div>',
  3729. )
  3730. expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  3731. })
  3732. // test('not enough children', () => {
  3733. // const { container } = mountWithHydration(
  3734. // `<div data-allow-mismatch="children"></div>`,
  3735. // () => h('div', [h('span', 'foo'), h('span', 'bar')]),
  3736. // )
  3737. // expect(container.innerHTML).toBe(
  3738. // '<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>',
  3739. // )
  3740. // expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  3741. // })
  3742. // test('too many children', () => {
  3743. // const { container } = mountWithHydration(
  3744. // `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
  3745. // () => h('div', [h('span', 'foo')]),
  3746. // )
  3747. // expect(container.innerHTML).toBe(
  3748. // '<div data-allow-mismatch="children"><span>foo</span></div>',
  3749. // )
  3750. // expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  3751. // })
  3752. test('complete mismatch', async () => {
  3753. const { container } = await mountWithHydration(
  3754. `<div data-allow-mismatch="children"><div>foo</div></div>`,
  3755. `<div><component :is="data">foo</component></div>`,
  3756. ref('span'),
  3757. )
  3758. expect(container.innerHTML).toBe(
  3759. '<div data-allow-mismatch="children"><span>foo</span><!--dynamic-component--></div>',
  3760. )
  3761. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  3762. })
  3763. // test('fragment mismatch removal', () => {
  3764. // const { container } = mountWithHydration(
  3765. // `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
  3766. // () => h('div', [h('span', 'replaced')]),
  3767. // )
  3768. // expect(container.innerHTML).toBe(
  3769. // '<div data-allow-mismatch="children"><span>replaced</span></div>',
  3770. // )
  3771. // expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  3772. // })
  3773. // test('fragment not enough children', () => {
  3774. // const { container } = mountWithHydration(
  3775. // `<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
  3776. // () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
  3777. // )
  3778. // expect(container.innerHTML).toBe(
  3779. // '<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
  3780. // )
  3781. // expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  3782. // })
  3783. // test('fragment too many children', () => {
  3784. // const { container } = mountWithHydration(
  3785. // `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
  3786. // () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
  3787. // )
  3788. // expect(container.innerHTML).toBe(
  3789. // '<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>',
  3790. // )
  3791. // // fragment ends early and attempts to hydrate the extra <div>bar</div>
  3792. // // as 2nd fragment child.
  3793. // expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
  3794. // // excessive children removal
  3795. // expect(`Hydration children mismatch`).not.toHaveBeenWarned()
  3796. // })
  3797. // test('comment mismatch (element)', () => {
  3798. // const { container } = mountWithHydration(
  3799. // `<div data-allow-mismatch="children"><span></span></div>`,
  3800. // () => h('div', [createCommentVNode('hi')]),
  3801. // )
  3802. // expect(container.innerHTML).toBe(
  3803. // '<div data-allow-mismatch="children"><!--hi--></div>',
  3804. // )
  3805. // expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  3806. // })
  3807. // test('comment mismatch (text)', () => {
  3808. // const { container } = mountWithHydration(
  3809. // `<div data-allow-mismatch="children">foobar</div>`,
  3810. // () => h('div', [createCommentVNode('hi')]),
  3811. // )
  3812. // expect(container.innerHTML).toBe(
  3813. // '<div data-allow-mismatch="children"><!--hi--></div>',
  3814. // )
  3815. // expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  3816. // })
  3817. test('class mismatch', async () => {
  3818. await mountWithHydration(
  3819. `<div class="foo bar" data-allow-mismatch="class"></div>`,
  3820. `<div :class="data"></div>`,
  3821. ref('foo'),
  3822. )
  3823. expect(`Hydration class mismatch`).not.toHaveBeenWarned()
  3824. })
  3825. test('style mismatch', async () => {
  3826. await mountWithHydration(
  3827. `<div style="color:red;" data-allow-mismatch="style"></div>`,
  3828. `<div :style="data"></div>`,
  3829. ref({ color: 'green' }),
  3830. )
  3831. expect(`Hydration style mismatch`).not.toHaveBeenWarned()
  3832. })
  3833. test('attr mismatch', async () => {
  3834. await mountWithHydration(
  3835. `<div data-allow-mismatch="attribute"></div>`,
  3836. `<div :id="data"></div>`,
  3837. ref('foo'),
  3838. )
  3839. await mountWithHydration(
  3840. `<div id="bar" data-allow-mismatch="attribute"></div>`,
  3841. `<div :id="data"></div>`,
  3842. ref('foo'),
  3843. )
  3844. expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
  3845. })
  3846. })
  3847. describe('VDOM interop', () => {
  3848. test('basic render vapor component', async () => {
  3849. const data = ref(true)
  3850. const { container } = await testWithVDOMApp(
  3851. `<script setup>const data = _data; const components = _components;</script>
  3852. <template>
  3853. <components.VaporChild/>
  3854. </template>`,
  3855. {
  3856. VaporChild: {
  3857. code: `<template>{{ data }}</template>`,
  3858. vapor: true,
  3859. },
  3860. },
  3861. data,
  3862. )
  3863. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"true"`)
  3864. data.value = false
  3865. await nextTick()
  3866. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"false"`)
  3867. })
  3868. test('nested components (VDOM -> Vapor -> VDOM)', async () => {
  3869. const data = ref(true)
  3870. const { container } = await testWithVDOMApp(
  3871. `<script setup>const data = _data; const components = _components;</script>
  3872. <template>
  3873. <components.VaporChild/>
  3874. </template>`,
  3875. {
  3876. VaporChild: {
  3877. code: `<template><components.VdomChild/></template>`,
  3878. vapor: true,
  3879. },
  3880. VdomChild: {
  3881. code: `<script setup>const data = _data;</script>
  3882. <template>{{ data }}</template>`,
  3883. vapor: false,
  3884. },
  3885. },
  3886. data,
  3887. )
  3888. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"true"`)
  3889. data.value = false
  3890. await nextTick()
  3891. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"false"`)
  3892. })
  3893. test('nested components (VDOM -> Vapor -> VDOM (with slot fallback))', async () => {
  3894. const data = ref(true)
  3895. const { container } = await testWithVDOMApp(
  3896. `<script setup>const data = _data; const components = _components;</script>
  3897. <template>
  3898. <components.VaporChild/>
  3899. </template>`,
  3900. {
  3901. VaporChild: {
  3902. code: `<template><components.VdomChild/></template>`,
  3903. vapor: true,
  3904. },
  3905. VdomChild: {
  3906. code: `<script setup>const data = _data;</script>
  3907. <template><slot><span>{{data}}</span></slot></template>`,
  3908. vapor: false,
  3909. },
  3910. },
  3911. data,
  3912. )
  3913. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3914. `
  3915. "
  3916. <!--[--><span>true</span><!--]-->
  3917. "
  3918. `,
  3919. )
  3920. data.value = false
  3921. await nextTick()
  3922. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3923. `
  3924. "
  3925. <!--[--><span>false</span><!--]-->
  3926. "
  3927. `,
  3928. )
  3929. })
  3930. test('nested components (VDOM -> Vapor(with slot content) -> VDOM)', async () => {
  3931. const data = ref(true)
  3932. const { container } = await testWithVDOMApp(
  3933. `<script setup>const data = _data; const components = _components;</script>
  3934. <template>
  3935. <components.VaporChild/>
  3936. </template>`,
  3937. {
  3938. VaporChild: {
  3939. code: `<template>
  3940. <components.VdomChild>
  3941. <template #default>
  3942. <span>{{data}} vapor fallback</span>
  3943. </template>
  3944. </components.VdomChild>
  3945. </template>`,
  3946. vapor: true,
  3947. },
  3948. VdomChild: {
  3949. code: `<script setup>const data = _data;</script>
  3950. <template><slot><span>vdom fallback</span></slot></template>`,
  3951. vapor: false,
  3952. },
  3953. },
  3954. data,
  3955. )
  3956. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3957. `
  3958. "
  3959. <!--[--><span>true vapor fallback</span><!--]-->
  3960. "
  3961. `,
  3962. )
  3963. data.value = false
  3964. await nextTick()
  3965. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3966. `
  3967. "
  3968. <!--[--><span>false vapor fallback</span><!--]-->
  3969. "
  3970. `,
  3971. )
  3972. })
  3973. test('nested components (VDOM -> Vapor(with slot content) -> Vapor)', async () => {
  3974. const data = ref(true)
  3975. const { container } = await testWithVDOMApp(
  3976. `<script setup>const data = _data; const components = _components;</script>
  3977. <template>
  3978. <components.VaporChild/>
  3979. </template>`,
  3980. {
  3981. VaporChild: {
  3982. code: `<template>
  3983. <components.VaporChild2>
  3984. <template #default>
  3985. <span>{{data}} vapor fallback</span>
  3986. </template>
  3987. </components.VaporChild2>
  3988. </template>`,
  3989. vapor: true,
  3990. },
  3991. VaporChild2: {
  3992. code: `<template><slot><span>vapor fallback2</span></slot></template>`,
  3993. vapor: true,
  3994. },
  3995. },
  3996. data,
  3997. )
  3998. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  3999. `
  4000. "
  4001. <!--[--><span>true vapor fallback</span><!--]-->
  4002. "
  4003. `,
  4004. )
  4005. data.value = false
  4006. await nextTick()
  4007. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  4008. `
  4009. "
  4010. <!--[--><span>false vapor fallback</span><!--]-->
  4011. "
  4012. `,
  4013. )
  4014. })
  4015. test('vapor slot render vdom component', async () => {
  4016. const data = ref(true)
  4017. const { container } = await testWithVDOMApp(
  4018. `<script setup>const data = _data; const components = _components;</script>
  4019. <template>
  4020. <components.VaporChild>
  4021. <components.VdomChild/>
  4022. </components.VaporChild>
  4023. </template>`,
  4024. {
  4025. VaporChild: {
  4026. code: `<template><div><slot/></div></template>`,
  4027. vapor: true,
  4028. },
  4029. VdomChild: {
  4030. code: `<script setup>const data = _data;</script>
  4031. <template>{{ data }}</template>`,
  4032. vapor: false,
  4033. },
  4034. },
  4035. data,
  4036. )
  4037. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  4038. `
  4039. "<div>
  4040. <!--[-->true<!--]-->
  4041. </div>"
  4042. `,
  4043. )
  4044. data.value = false
  4045. await nextTick()
  4046. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  4047. `
  4048. "<div>
  4049. <!--[-->false<!--]-->
  4050. </div>"
  4051. `,
  4052. )
  4053. })
  4054. test('vapor slot render vdom component (render function)', async () => {
  4055. const data = ref(true)
  4056. const { container } = await testWithVaporApp(
  4057. `<script setup>
  4058. import { h } from 'vue'
  4059. const data = _data; const components = _components;
  4060. const VdomChild = {
  4061. setup() {
  4062. return () => h('div', null, [h('div', [String(data.value)])])
  4063. }
  4064. }
  4065. </script>
  4066. <template>
  4067. <components.VaporChild>
  4068. <VdomChild/>
  4069. </components.VaporChild>
  4070. </template>`,
  4071. {
  4072. VaporChild: {
  4073. code: `<template><div><slot/></div></template>`,
  4074. vapor: true,
  4075. },
  4076. },
  4077. data,
  4078. )
  4079. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  4080. `
  4081. "<div>
  4082. <!--[--><div><div>true</div></div><!--]-->
  4083. </div>"
  4084. `,
  4085. )
  4086. expect(`Hydration node mismatch`).not.toHaveBeenWarned()
  4087. data.value = false
  4088. await nextTick()
  4089. expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
  4090. `
  4091. "<div>
  4092. <!--[--><div><div>false</div></div><!--]-->
  4093. </div>"
  4094. `,
  4095. )
  4096. })
  4097. })