hydration.spec.ts 154 KB

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